From 053d02ef3538708f3679705588d8319ca56c6b88 Mon Sep 17 00:00:00 2001 From: Benjamin Gruenbaum Date: Fri, 3 Mar 2017 23:41:19 +0200 Subject: [PATCH] add tapCatch (#1220) --- docs/docs/api-reference.md | 1 + docs/docs/api/tapcatch.md | 128 ++++++++++++++++++++++++++++++++++++ src/finally.js | 39 ++++++++++- src/promise.js | 2 +- test/mocha/tapCatch.js | 130 +++++++++++++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 docs/docs/api/tapcatch.md create mode 100644 test/mocha/tapCatch.js diff --git a/docs/docs/api-reference.md b/docs/docs/api-reference.md index c8d13ed0c..778ff626a 100644 --- a/docs/docs/api-reference.md +++ b/docs/docs/api-reference.md @@ -67,6 +67,7 @@ redirect_from: "/docs/api/index.html" - [Promise.coroutine.addYieldHandler](api/promise.coroutine.addyieldhandler.html) - [Utility](api/utility.html) - [.tap](api/tap.html) + - [.tapCatch](api/tapCatch.html) - [.call](api/call.html) - [.get](api/get.html) - [.return](api/return.html) diff --git a/docs/docs/api/tapcatch.md b/docs/docs/api/tapcatch.md new file mode 100644 index 000000000..079d30209 --- /dev/null +++ b/docs/docs/api/tapcatch.md @@ -0,0 +1,128 @@ +--- +layout: api +id: tapCatch +title: .tapCatch +--- + + +[← Back To API Reference](/docs/api-reference.html) +
+##.tapCatch + + +`.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. + +It comes in two variants. + - A tapCatch-all variant similar to [`.catch`](.) block. This variant is compatible with native promises. + - A filtered variant (like other non-JS languages typically have) that lets you only handle specific errors. **This variant is usually preferable**. + + +### `tapCatch` all +```js +.tapCatch(function(any value) handler) -> Promise +``` + + +Like [`.finally`](.) that is not called for fulfillments. + +```js +getUser().tapCatch(function(err) { + return logErrorToDatabase(err); +}).then(function(user) { + //user is the user from getUser(), not logErrorToDatabase() +}); +``` + +Common case includes adding logging to an existing promise chain: + +**Rate Limiting** +``` +Promise. + try(logIn). + then(respondWithSuccess). + tapCatch(countFailuresForRateLimitingPurposes). + catch(respondWithError); +``` + +**Circuit Breakers** +``` +Promise. + try(makeRequest). + then(respondWithSuccess). + tapCatch(adjustCircuitBreakerState). + catch(respondWithError); +``` + +**Logging** +``` +Promise. + try(doAThing). + tapCatch(logErrorsRelatedToThatThing). + then(respondWithSuccess). + catch(respondWithError); +``` +*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.* + +### Filtered `tapCatch` + + +```js +.tapCatch( + class ErrorClass|function(any error), + function(any error) handler +) -> Promise +``` +```js +.tapCatch( + class ErrorClass|function(any error), + function(any error) handler +) -> Promise + + +``` +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. + +Usage examples include: + +**Rate Limiting** +``` +Bluebird. + try(logIn). + then(respondWithSuccess). + tapCatch(InvalidCredentialsError, countFailuresForRateLimitingPurposes). + catch(respondWithError); +``` + +**Circuit Breakers** +``` +Bluebird. + try(makeRequest). + then(respondWithSuccess). + tapCatch(RequestError, adjustCircuitBreakerState). + catch(respondWithError); +``` + +**Logging** +``` +Bluebird. + try(doAThing). + tapCatch(logErrorsRelatedToThatThing). + then(respondWithSuccess). + catch(respondWithError); +``` + +
+ +
+ + diff --git a/src/finally.js b/src/finally.js index 6521e7d6d..f69957335 100644 --- a/src/finally.js +++ b/src/finally.js @@ -1,8 +1,9 @@ "use strict"; -module.exports = function(Promise, tryConvertToPromise) { +module.exports = function(Promise, tryConvertToPromise, NEXT_FILTER) { var util = require("./util"); var CancellationError = Promise.CancellationError; var errorObj = util.errorObj; +var catchFilter = require("./catch_filter")(NEXT_FILTER); function PassThroughHandlerContext(promise, type, handler) { this.promise = promise; @@ -54,7 +55,9 @@ function finallyHandler(reasonOrValue) { var ret = this.isFinallyHandler() ? handler.call(promise._boundValue()) : handler.call(promise._boundValue(), reasonOrValue); - if (ret !== undefined) { + if (ret === NEXT_FILTER) { + return ret; + } else if (ret !== undefined) { promise._setReturnedNonUndefined(); var maybePromise = tryConvertToPromise(ret, promise); if (maybePromise instanceof Promise) { @@ -103,9 +106,41 @@ Promise.prototype["finally"] = function (handler) { finallyHandler); }; + Promise.prototype.tap = function (handler) { return this._passThrough(handler, TAP_TYPE, finallyHandler); }; +Promise.prototype.tapCatch = function (handlerOrPredicate) { + var len = arguments.length; + if(len === 1) { + return this._passThrough(handlerOrPredicate, + TAP_TYPE, + undefined, + finallyHandler); + } else { + var catchInstances = new Array(len - 1), + j = 0, i; + for (i = 0; i < len - 1; ++i) { + var item = arguments[i]; + if (util.isObject(item)) { + catchInstances[j++] = item; + } else { + return Promise.reject(new TypeError( + "tapCatch statement predicate: " + + OBJECT_ERROR + util.classString(item) + )); + } + } + catchInstances.length = j; + var handler = arguments[i]; + return this._passThrough(catchFilter(catchInstances, handler, this), + TAP_TYPE, + undefined, + finallyHandler); + } + +}; + return PassThroughHandlerContext; }; diff --git a/src/promise.js b/src/promise.js index 730cad4d5..b1cd220ee 100644 --- a/src/promise.js +++ b/src/promise.js @@ -53,7 +53,7 @@ var createContext = Context.create; var debug = require("./debuggability")(Promise, Context); var CapturedTrace = debug.CapturedTrace; var PassThroughHandlerContext = - require("./finally")(Promise, tryConvertToPromise); + require("./finally")(Promise, tryConvertToPromise, NEXT_FILTER); var catchFilter = require("./catch_filter")(NEXT_FILTER); var nodebackForPromise = require("./nodeback"); var errorObj = util.errorObj; diff --git a/test/mocha/tapCatch.js b/test/mocha/tapCatch.js new file mode 100644 index 000000000..e141ea4b1 --- /dev/null +++ b/test/mocha/tapCatch.js @@ -0,0 +1,130 @@ +"use strict"; +var assert = require("assert"); +var testUtils = require("./helpers/util.js"); +function rejection() { + var error = new Error("test"); + var rejection = Promise.reject(error); + rejection.err = error; + return rejection; +} + +describe("tapCatch", function () { + + specify("passes through rejection reason", function() { + return rejection().tapCatch(function() { + return 3; + }).caught(function(value) { + assert.equal(value.message, "test"); + }); + }); + + specify("passes through reason after returned promise is fulfilled", function() { + var async = false; + return rejection().tapCatch(function() { + return new Promise(function(r) { + setTimeout(function(){ + async = true; + r(3); + }, 1); + }); + }).caught(function(value) { + assert(async); + assert.equal(value.message, "test"); + }); + }); + + specify("is not called on fulfilled promise", function() { + var called = false; + return Promise.resolve("test").tapCatch(function() { + called = true; + }).then(function(value){ + assert(!called); + }, assert.fail); + }); + + specify("passes immediate rejection", function() { + var err = new Error(); + return rejection().tapCatch(function() { + throw err; + }).tap(assert.fail).then(assert.fail, function(e) { + assert(err === e); + }); + }); + + specify("passes eventual rejection", function() { + var err = new Error(); + return rejection().tapCatch(function() { + return new Promise(function(_, rej) { + setTimeout(function(){ + rej(err); + }, 1) + }); + }).tap(assert.fail).then(assert.fail, function(e) { + assert(err === e); + }); + }); + + specify("passes reason", function() { + return rejection().tapCatch(function(a) { + assert(a === rejection); + }).then(assert.fail, function() {}); + }); + + specify("Works with predicates", function() { + var called = false; + return Promise.reject(new TypeError).tapCatch(TypeError, function(a) { + called = true; + assert(err instanceof TypeError) + }).then(assert.fail, function(err) { + assert(called === true); + assert(err instanceof TypeError); + }); + }); + specify("Does not get called on predicates that don't match", function() { + var called = false; + return Promise.reject(new TypeError).tapCatch(ReferenceError, function(a) { + called = true; + }).then(assert.fail, function(err) { + assert(called === false); + assert(err instanceof TypeError); + }); + }); + + specify("Supports multiple predicates", function() { + var calledA = false; + var calledB = false; + var calledC = false; + + var promiseA = Promise.reject(new ReferenceError).tapCatch( + ReferenceError, + TypeError, + function (e) { + assert(e instanceof ReferenceError); + calledA = true; + } + ).catch(function () {}); + + var promiseB = Promise.reject(new TypeError).tapCatch( + ReferenceError, + TypeError, + function (e) { + assert(e instanceof TypeError); + calledB = true; + } + ).catch(function () {}); + + var promiseC = Promise.reject(new SyntaxError).tapCatch( + ReferenceError, + TypeError, + function (e) { + calledC = true; + } + ).catch(function () {}); + + return Promise.join(promiseA, promiseB, promiseC, function () { + assert(calledA === true); + assert(calledB === true); + assert(calledC === false); + }); + }) +});