Skip to content

Commit 053d02e

Browse files
benjamingrpetkaantonov
authored andcommitted
add tapCatch (petkaantonov#1220)
1 parent 8d52820 commit 053d02e

File tree

5 files changed

+297
-3
lines changed

5 files changed

+297
-3
lines changed

docs/docs/api-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ redirect_from: "/docs/api/index.html"
6767
- [Promise.coroutine.addYieldHandler](api/promise.coroutine.addyieldhandler.html)
6868
- [Utility](api/utility.html)
6969
- [.tap](api/tap.html)
70+
- [.tapCatch](api/tapCatch.html)
7071
- [.call](api/call.html)
7172
- [.get](api/get.html)
7273
- [.return](api/return.html)

docs/docs/api/tapcatch.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
layout: api
3+
id: tapCatch
4+
title: .tapCatch
5+
---
6+
7+
8+
[← Back To API Reference](/docs/api-reference.html)
9+
<div class="api-code-section"><markdown>
10+
##.tapCatch
11+
12+
13+
`.tapCatch` is a convenience method for reacting to errors without handling them with promises - similar to `finally` but only called on rejections. Useful for logging errors.
14+
15+
It comes in two variants.
16+
- A tapCatch-all variant similar to [`.catch`](.) block. This variant is compatible with native promises.
17+
- A filtered variant (like other non-JS languages typically have) that lets you only handle specific errors. **This variant is usually preferable**.
18+
19+
20+
### `tapCatch` all
21+
```js
22+
.tapCatch(function(any value) handler) -> Promise
23+
```
24+
25+
26+
Like [`.finally`](.) that is not called for fulfillments.
27+
28+
```js
29+
getUser().tapCatch(function(err) {
30+
return logErrorToDatabase(err);
31+
}).then(function(user) {
32+
//user is the user from getUser(), not logErrorToDatabase()
33+
});
34+
```
35+
36+
Common case includes adding logging to an existing promise chain:
37+
38+
**Rate Limiting**
39+
```
40+
Promise.
41+
try(logIn).
42+
then(respondWithSuccess).
43+
tapCatch(countFailuresForRateLimitingPurposes).
44+
catch(respondWithError);
45+
```
46+
47+
**Circuit Breakers**
48+
```
49+
Promise.
50+
try(makeRequest).
51+
then(respondWithSuccess).
52+
tapCatch(adjustCircuitBreakerState).
53+
catch(respondWithError);
54+
```
55+
56+
**Logging**
57+
```
58+
Promise.
59+
try(doAThing).
60+
tapCatch(logErrorsRelatedToThatThing).
61+
then(respondWithSuccess).
62+
catch(respondWithError);
63+
```
64+
*Note: in browsers it is necessary to call `.tapCatch` with `console.log.bind(console)` because console methods can not be called as stand-alone functions.*
65+
66+
### Filtered `tapCatch`
67+
68+
69+
```js
70+
.tapCatch(
71+
class ErrorClass|function(any error),
72+
function(any error) handler
73+
) -> Promise
74+
```
75+
```js
76+
.tapCatch(
77+
class ErrorClass|function(any error),
78+
function(any error) handler
79+
) -> Promise
80+
81+
82+
```
83+
This is an extension to [`.tapCatch`](.) to filter exceptions similarly to languages like Java or C#. Instead of manually checking `instanceof` or `.name === "SomeError"`, you may specify a number of error constructors which are eligible for this tapCatch handler. The tapCatch handler that is first met that has eligible constructors specified, is the one that will be called.
84+
85+
Usage examples include:
86+
87+
**Rate Limiting**
88+
```
89+
Bluebird.
90+
try(logIn).
91+
then(respondWithSuccess).
92+
tapCatch(InvalidCredentialsError, countFailuresForRateLimitingPurposes).
93+
catch(respondWithError);
94+
```
95+
96+
**Circuit Breakers**
97+
```
98+
Bluebird.
99+
try(makeRequest).
100+
then(respondWithSuccess).
101+
tapCatch(RequestError, adjustCircuitBreakerState).
102+
catch(respondWithError);
103+
```
104+
105+
**Logging**
106+
```
107+
Bluebird.
108+
try(doAThing).
109+
tapCatch(logErrorsRelatedToThatThing).
110+
then(respondWithSuccess).
111+
catch(respondWithError);
112+
```
113+
114+
</markdown></div>
115+
116+
<div id="disqus_thread"></div>
117+
<script type="text/javascript">
118+
var disqus_title = ".tap";
119+
var disqus_shortname = "bluebirdjs";
120+
var disqus_identifier = "disqus-id-tap";
121+
122+
(function() {
123+
var dsq = document.createElement("script"); dsq.type = "text/javascript"; dsq.async = true;
124+
dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";
125+
(document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(dsq);
126+
})();
127+
</script>
128+
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript>

src/finally.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use strict";
2-
module.exports = function(Promise, tryConvertToPromise) {
2+
module.exports = function(Promise, tryConvertToPromise, NEXT_FILTER) {
33
var util = require("./util");
44
var CancellationError = Promise.CancellationError;
55
var errorObj = util.errorObj;
6+
var catchFilter = require("./catch_filter")(NEXT_FILTER);
67

78
function PassThroughHandlerContext(promise, type, handler) {
89
this.promise = promise;
@@ -54,7 +55,9 @@ function finallyHandler(reasonOrValue) {
5455
var ret = this.isFinallyHandler()
5556
? handler.call(promise._boundValue())
5657
: handler.call(promise._boundValue(), reasonOrValue);
57-
if (ret !== undefined) {
58+
if (ret === NEXT_FILTER) {
59+
return ret;
60+
} else if (ret !== undefined) {
5861
promise._setReturnedNonUndefined();
5962
var maybePromise = tryConvertToPromise(ret, promise);
6063
if (maybePromise instanceof Promise) {
@@ -103,9 +106,41 @@ Promise.prototype["finally"] = function (handler) {
103106
finallyHandler);
104107
};
105108

109+
106110
Promise.prototype.tap = function (handler) {
107111
return this._passThrough(handler, TAP_TYPE, finallyHandler);
108112
};
109113

114+
Promise.prototype.tapCatch = function (handlerOrPredicate) {
115+
var len = arguments.length;
116+
if(len === 1) {
117+
return this._passThrough(handlerOrPredicate,
118+
TAP_TYPE,
119+
undefined,
120+
finallyHandler);
121+
} else {
122+
var catchInstances = new Array(len - 1),
123+
j = 0, i;
124+
for (i = 0; i < len - 1; ++i) {
125+
var item = arguments[i];
126+
if (util.isObject(item)) {
127+
catchInstances[j++] = item;
128+
} else {
129+
return Promise.reject(new TypeError(
130+
"tapCatch statement predicate: "
131+
+ OBJECT_ERROR + util.classString(item)
132+
));
133+
}
134+
}
135+
catchInstances.length = j;
136+
var handler = arguments[i];
137+
return this._passThrough(catchFilter(catchInstances, handler, this),
138+
TAP_TYPE,
139+
undefined,
140+
finallyHandler);
141+
}
142+
143+
};
144+
110145
return PassThroughHandlerContext;
111146
};

src/promise.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ var createContext = Context.create;
5353
var debug = require("./debuggability")(Promise, Context);
5454
var CapturedTrace = debug.CapturedTrace;
5555
var PassThroughHandlerContext =
56-
require("./finally")(Promise, tryConvertToPromise);
56+
require("./finally")(Promise, tryConvertToPromise, NEXT_FILTER);
5757
var catchFilter = require("./catch_filter")(NEXT_FILTER);
5858
var nodebackForPromise = require("./nodeback");
5959
var errorObj = util.errorObj;

test/mocha/tapCatch.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"use strict";
2+
var assert = require("assert");
3+
var testUtils = require("./helpers/util.js");
4+
function rejection() {
5+
var error = new Error("test");
6+
var rejection = Promise.reject(error);
7+
rejection.err = error;
8+
return rejection;
9+
}
10+
11+
describe("tapCatch", function () {
12+
13+
specify("passes through rejection reason", function() {
14+
return rejection().tapCatch(function() {
15+
return 3;
16+
}).caught(function(value) {
17+
assert.equal(value.message, "test");
18+
});
19+
});
20+
21+
specify("passes through reason after returned promise is fulfilled", function() {
22+
var async = false;
23+
return rejection().tapCatch(function() {
24+
return new Promise(function(r) {
25+
setTimeout(function(){
26+
async = true;
27+
r(3);
28+
}, 1);
29+
});
30+
}).caught(function(value) {
31+
assert(async);
32+
assert.equal(value.message, "test");
33+
});
34+
});
35+
36+
specify("is not called on fulfilled promise", function() {
37+
var called = false;
38+
return Promise.resolve("test").tapCatch(function() {
39+
called = true;
40+
}).then(function(value){
41+
assert(!called);
42+
}, assert.fail);
43+
});
44+
45+
specify("passes immediate rejection", function() {
46+
var err = new Error();
47+
return rejection().tapCatch(function() {
48+
throw err;
49+
}).tap(assert.fail).then(assert.fail, function(e) {
50+
assert(err === e);
51+
});
52+
});
53+
54+
specify("passes eventual rejection", function() {
55+
var err = new Error();
56+
return rejection().tapCatch(function() {
57+
return new Promise(function(_, rej) {
58+
setTimeout(function(){
59+
rej(err);
60+
}, 1)
61+
});
62+
}).tap(assert.fail).then(assert.fail, function(e) {
63+
assert(err === e);
64+
});
65+
});
66+
67+
specify("passes reason", function() {
68+
return rejection().tapCatch(function(a) {
69+
assert(a === rejection);
70+
}).then(assert.fail, function() {});
71+
});
72+
73+
specify("Works with predicates", function() {
74+
var called = false;
75+
return Promise.reject(new TypeError).tapCatch(TypeError, function(a) {
76+
called = true;
77+
assert(err instanceof TypeError)
78+
}).then(assert.fail, function(err) {
79+
assert(called === true);
80+
assert(err instanceof TypeError);
81+
});
82+
});
83+
specify("Does not get called on predicates that don't match", function() {
84+
var called = false;
85+
return Promise.reject(new TypeError).tapCatch(ReferenceError, function(a) {
86+
called = true;
87+
}).then(assert.fail, function(err) {
88+
assert(called === false);
89+
assert(err instanceof TypeError);
90+
});
91+
});
92+
93+
specify("Supports multiple predicates", function() {
94+
var calledA = false;
95+
var calledB = false;
96+
var calledC = false;
97+
98+
var promiseA = Promise.reject(new ReferenceError).tapCatch(
99+
ReferenceError,
100+
TypeError,
101+
function (e) {
102+
assert(e instanceof ReferenceError);
103+
calledA = true;
104+
}
105+
).catch(function () {});
106+
107+
var promiseB = Promise.reject(new TypeError).tapCatch(
108+
ReferenceError,
109+
TypeError,
110+
function (e) {
111+
assert(e instanceof TypeError);
112+
calledB = true;
113+
}
114+
).catch(function () {});
115+
116+
var promiseC = Promise.reject(new SyntaxError).tapCatch(
117+
ReferenceError,
118+
TypeError,
119+
function (e) {
120+
calledC = true;
121+
}
122+
).catch(function () {});
123+
124+
return Promise.join(promiseA, promiseB, promiseC, function () {
125+
assert(calledA === true);
126+
assert(calledB === true);
127+
assert(calledC === false);
128+
});
129+
})
130+
});

0 commit comments

Comments
 (0)