Implementing Q's .allSettled() Promise Method In Bluebird
Historically, I've used the Q Promise library by Kris Kowal because it happend to be the library that was modelled in AngularJS. One of the nice features of Q is that it has an .allSettled() static method that allows you to collect both fulfillments and rejections in a promise resolution. At work, my current team has opted to use Bluebird, an alternate Promise library, that doesn't have an .allSettled() method. As such, I thought it would be fun to augment the Bluebird library to include an .allSettled() implementation.
When augmenting the Bluebird library, you want to make sure that you don't accidentally clobber other libraries that depend on a known configuration of Bluebird. Therefore, in order to safely augment Bluebird, the docs recommend that you use the .getNewLibraryCopy() method to create a locally-scoped version of Bluebird for your application logic. Once you have a locally-scopes version, you can safely alter its prototype methods and static methods without affecting the core Bluebird configuration.
The key feature of the .allSettled() method is that it allows us to collect Promise rejections rather than propagate them to the closest .catch() handler. To implement this facet of functionality, we could attach a .catch() handler to each promise that transforms a rejection into a fulfillment. Or, we could use Bluebird's native .reflect() method. The .reflect() method does exactly what our custom .catch() handler would do, which is to convert rejections into fulfillments using an expected "state" format.
In the following exploration, I'm using the .reflect() method in order to aggregate both fulfillment and rejection results; but, I'm also using an additional .then() handler in order to format the results in accordance with Q's implementation. If you didn't care about Q's implementation, you could simply use .reflect() and call it a day.
As a learning experience, I'm also providing a prototype method - .rejected() - that filters .allSettled() results down to just the rejected ones.
// Require the core node modules.
var bluebirdCore = require( "bluebird" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Create a scoped version of the bluebird library for this application. This way, we can
// mess with the instance and prototype methods without changing the configuration of the
// bluebird library that may be used by other dependencies.
// --
// READ MORE: http://bluebirdjs.com/docs/api/promise.getnewlibrarycopy.html
var bluebird = module.exports = bluebirdCore.getNewLibraryCopy();
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I resolve to a collection of settled values that match Q's implementation. Each result
// will contain a state of "fulfilled" or "rejected" and a "value" or "reason" property,
// respectively. Embedded rejections do not cause this promise to be rejected.
bluebird.allSettled = function( promises ) {
// We have to "wrap" each one of these promises in a reflect() call that will allow
// us to collection all of the results, regardless of whether or not the individual
// promises are resolved or rejected.
var reflectedPromsies = promises.map(
function operator( promise ) {
var promise = bluebird
.resolve( promise )
.reflect() // Always "resolves" to state inspection.
// NOTE: If I didn't care about being compatible with Q, I could omit
// the following handler and just return the result of .reflect().
.then(
function handleSettled( inspection ) {
if ( inspection.isFulfilled() ) {
return({
state: "fulfilled",
value: inspection.value()
});
} else {
return({
state: "rejected",
reason: inspection.reason()
});
}
}
)
;
return( promise );
}
);
return( bluebird.all( reflectedPromsies ) );
};
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I settle all of the given promises and then propagate only the rejections.
bluebird.rejected = function( promises ) {
return( this.allSettled( promises ).rejected() );
};
// I filter the .allSettled() results, returning only those that are rejections.
bluebird.prototype.rejected = function() {
var promise = this.then(
function handleSettled( results ) {
var rejectedResults = results.filter(
function( result ) {
return( result.state === "rejected" );
}
);
return( rejectedResults );
}
);
return( promise );
};
As you can see, the first thing I do is create and export a locally-scoped copy of Bluebird. This way, we know that the modifications we are about to make to the Bluebird class and prototype chain are only being applied to our local copy. Then, once we have a local copy, I'm injecting the .allSettled() and .rejected() methods.
In the rest of the application, we then need to be sure to require() our application-scoped copy of Bluebird rather than the globally-scope implementation:
// Require the core node modules.
var chalk = require( "chalk" );
// Require the application modules.
// --
// NOTE: We are not requiring the global bluebird module; instead, we're requiring our
// application-specific version that we know has been modified.
var bluebird = require( "./bluebird-extended" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// NOTE: Using native promises here (in order to demonstrate that our modified bluebird
// library safely normalizes non-bluebird promises).
var promises = [
Promise.resolve( "woot!" ),
Promise.reject( "meh!" ),
Promise.resolve( "sweet!" ),
Promise.reject( "lame!" )
];
// Wait for all of the promises to settle, then log out the results. This will catch
// any rejections internally and the just fold them into the results.
bluebird.allSettled( promises ).then(
function( results ) {
logHeader( ".allSettled().then()" );
for ( var result of results ) {
if ( result.state === "fulfilled" ) {
logFulfillment( result.value );
} else {
logRejection( result.reason );
}
}
}
);
// Wait for all of the promises to settle, filter for rejected, then log the rejections.
bluebird.rejected( promises ).then(
function( results ) {
logHeader( ".rejected().then()" );
for ( var result of results ) {
logRejection( result.reason );
}
}
);
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
function logHeader( value ) {
console.log( chalk.bold( value ) );
}
function logFulfillment( value ) {
console.log( chalk.dim( " ->" ), chalk.green.bold( "Fulfilled:" ), value );
}
function logRejection( value ) {
console.log( chalk.dim( " ->" ), chalk.red.bold( "Rejected:" ), value );
}
To test this all out, I'm creating a mixed collection of fulfilled and rejected promises. Then, I'm inspecting those promises using my application-scoped .allSettled() method and .rejected() method. And, when we run the above code, we get the following terminal output:
As you can see, our .allSettled() static method collected both the fulfillments and the rejections and propagated them all to the closest .then() handler. And, our .rejected() static method did the same, but filtered the results down to only the rejections before propagating them.
One of the great features of a Promise is that it will propagate a error to the closest .catch() handler. But, sometimes, if you're dealing with a collection of Promises, you don't want one error to hide the rest of errors. In those cases, it's great to be able to use something like .allSettled() to aggregate and consume the errors before moving on with your workflow.
Want to use code from this post? Check out the license.
Reader Comments
This is awesome and save my day :) !!
@Andrés,
Glad you found this helpful! Rock on :D