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);
+ });
+ })
+});