Bluebird is a promise library I’ve used for almost every project. I remember when I was learning promise a few years ago, it took me a while to understand it.
One day, I was searching for some related materials, I found Bluebird. I was wondering what’s the reason for using a third party library when Node.js already has native Promise support at that point? Well, it turns out there are plenty. Although some of its features can be achieved with today’s Node.js, bluebird still can give you a lot of benefits.
Performance#
Performance is one of the reasons I choose Bluebird at the very beginning. It’s very performant since…forever?
However, thanks to V8, Node 10 can now provide faster promise performance as well.
The following are the benchmark results on my laptop. You can try it yourself:
$ git clone https://github.com/petkaantonov/bluebird/
$ nvs use <node_version> # or nvm if you like
$ ./bench doxbee # or ./bench parallel depends on what you want to test
Node 10.16.0 doxbee#
file time(ms) memory(MB)
callbacks-baseline.js 132 24.89
callbacks-suguru03-neo-async-waterfall.js 159 40.16
callbacks-caolan-async-waterfall.js 203 45.71
promises-bluebird-generator.js 211 35.49
promises-bluebird.js 245 45.41
promises-cujojs-when.js 306 61.96
generators-tj-co.js 323 60.97
promises-lvivski-davy.js 389 85.11
promises-native-async-await.js 405 67.00
promises-then-promise.js 444 68.84
promises-calvinmetcalf-lie.js 489 125.57
promises-tildeio-rsvp.js 538 77.93
promises-dfilatov-vow.js 560 128.28
promises-ecmascript6-native.js 594 79.71
streamline-generators.js 799 89.75
observables-pozadi-kefir.js 856 155.93
promises-obvious-kew.js 882 107.42
promises-medikoo-deferred.js 900 136.11
streamline-callbacks.js 1350 106.45
observables-Reactive-Extensions-RxJS.js 1357 226.14
observables-caolan-highland.js 3467 473.56
promises-kriskowal-q.js 5242 289.95
observables-baconjs-bacon.js.js 5811 551.76
Platform info:
Darwin 18.6.0 x64
Node.JS 10.16.0
V8 6.8.275.32-node.52
Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz × 4
Node 10.16.0 Parallel#
file time(ms) memory(MB)
callbacks-baseline.js 290 84.93
callbacks-suguru03-neo-async-parallel.js 361 84.98
callbacks-caolan-async-parallel.js 450 113.71
promises-bluebird.js 454 104.64
promises-bluebird-generator.js 480 97.86
promises-lvivski-davy.js 578 159.97
promises-cujojs-when.js 668 170.84
generators-tj-co.js 998 240.93
promises-native-async-await.js 1030 231.30
promises-ecmascript6-native.js 1072 223.22
promises-tildeio-rsvp.js 1282 329.29
promises-then-promise.js 1373 288.88
promises-medikoo-deferred.js 1643 357.35
promises-calvinmetcalf-lie.js 1924 371.12
promises-dfilatov-vow.js 2483 535.41
promises-obvious-kew.js 3352 552.88
streamline-generators.js 6911 780.16
streamline-callbacks.js 11767 1134.09
Platform info:
Darwin 18.6.0 x64
Node.JS 10.16.0
V8 6.8.275.32-node.52
Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz × 4
Useful APIs#
.map, Promise.map#
Promise.map
is something like Promise.all
but more concise and has the capability of currency control.
Let’s take a look at the examples:
const Promise = require("bluebird");
const getNameAfter5Seconds = name => {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Name: ${name} (${new Date})`)
}, 5 * 1000);
});
};
(async () => {
const names = [ "John Doe", "Jane Doe", "Anonymous" ];
const tasks = names.map(getNameAfter5Seconds);
const results = await Promise.all(tasks);
results.forEach(r => console.log(r));
})();
And the output will be:
Name: John Doe (Sun Jul 21 2019 17:04:37 GMT+0800 (GMT+08:00))
Name: Jane Doe (Sun Jul 21 2019 17:04:37 GMT+0800 (GMT+08:00))
Name: Anonymous (Sun Jul 21 2019 17:04:37 GMT+0800 (GMT+08:00))
You see, normally when we use Promise.all
we would need an array of promises first. It’s not painful but still an extra step.
With Promise.map
you can achieve the same by:
(async () => {
const names = [ "John Doe", "Jane Doe", "Anonymous" ];
const results = await Promise.map(names, getNameAfter5Seconds);
results.forEach(r => console.log(r));
})();
Additionally, you can even chain it:
(async () => {
const names = [ "John Doe", "Jane Doe", "Anonymous" ];
return Promise.map(names, getNameAfter5Seconds) // or use Promise.all here
.map(r => {
console.log(r);
return r;
})
})();
By chaining this you are just using the previously resolved results as input of Promise.map
.
Last, let’s take a look at the currency control of Promise.map
, which can’t be achieved with Promise.all
.
It’s very straightforward, that means there won’t be more than the number of promises you specified running at the same time.
(async () => {
const names = [ "John Doe", "Jane Doe", "Anonymous" ];
return Promise.map(names, getNameAfter5Seconds, { concurrency: 2 })
.map(r => {
console.log(r);
return r;
})
})();
And the output is, yes, just like you expected:
Name: John Doe (Sun Jul 21 2019 17:20:04 GMT+0800 (GMT+08:00))
Name: Jane Doe (Sun Jul 21 2019 17:20:04 GMT+0800 (GMT+08:00))
Name: Anonymous (Sun Jul 21 2019 17:20:09 GMT+0800 (GMT+08:00))
It’s useful when you are doing something that might go wrong if the speed is too fast, like sending a bunch of HTTP requests. You can also combine this with a rate limit mechanism to properly manage your speed control.
.tap#
When we looked at this example:
(async () => {
const names = [ "John Doe", "Jane Doe", "Anonymous" ];
return Promise.map(names, getNameAfter5Seconds, { concurrency: 2 })
.map(r => {
console.log(r);
return r;
})
})();
Don’t you think it’s really redundant to just print something?
Yes, apparently everyone thinks so. What if we want to take a look what’ was returned without taking the risk of forgetting to pass it back?
Promise.resolve(1000)
.then(n => console.log(`The value is: ${n}`)); // We didn't return anything!
.then(number => console.log(`I got: ${number}`)); // number is now undefined
That’s where .tap
can be useful. We can literally “tap” on the promise without worrying about forgetting to return value.
Promise.resolve(1000)
.tap(n => console.log(`The value is: ${n}`)); // We didn't return anything!
.then(number => console.log(`I got: ${number}`)); // number is 1000
You might wonder, what if we return something else in the .tap
’s callback?
Promise.resolve(1000)
.tap(() => 2000)
.then(number => console.log(`I got: ${number}`)); // number is still 1000
Of course, you can do anything shady with .tap
if you like. But most of the time you might just want to print something, you can use a simple technique like:
Promise.resolve(1000)
.then(n => console.log(n) || n)
.then(number => console.log(`I got: ${number}`)); // number is still 1000
Or, just use VSCode’s logpoint?
.call#
We often deal with objects with one-liners, like:
Promise.resolve([ 1, 2 ,3 ,4 ,5 ])
.then(arr => arr.sort((a, b) => b - a));
While can be done with:
Promise.resolve([ 1, 2 ,3 ,4 ,5 ])
.call("sort", (a, b) => b - a);
For more examples, see .call
.
.reflect#
When we do something to a collection, we need to make sure when some of them got rejected, the remaining promises won’t be affected.
const Promise = require("bluebird");
const rejectOnEven = number => new Promise((resolve, reject) => {
return number % 2 !== 0? resolve(number) : reject(`${number} is rejected because it's even`);
});
(async () => {
const tasks = [ 1, 2, 3 ,4 ].map(rejectOnEven);
const results = await Promise.all(tasks);
console.log(results);
})()
And the output will be:
(node:66335) UnhandledPromiseRejectionWarning: 2
(node:66335) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:66335) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
As you expected, the remaining promises won’t have the chance to finish. Of course, we can handle it in the rejectOnEven
function, but can we deal with these later, and just let all promises settled?
If you are using Q, there is an .allSettled
for you.
As for bluebird, we have .reflect
.
Let’s modify the example:
(async () => {
const tasks = [ 1, 2, 3 ,4 ].map(rejectOnEven);
const results = await Promise.all(tasks.map(t => t.reflect()));
results.forEach(r => {
if (r.isFulfilled()) {
console.log(`Resolved value: ${r.value()}`);
} else {
console.log(`Rejected reason: ${r.reason()}`);
}
})
})();
And the output:
Resolved value: 1
Rejected reason: 2 is rejected because it's even
Resolved value: 3
Rejected reason: 4 is rejected because it's even
One thing to notice is, it seems you can’t replace the Promise.all
here with Promise.map
.
…and others#
The examples above are just something I used frequently. There are more. I believe you will find others useful as well.
Promisify, PromisifyAll#
When your dependencies are still using callback style, you can easily transform it to promise-based style For example:
const Promise = require("bluebird");
const fs = Promise.promisifyAll(require("fs"));
Then you have something like fs.readFileAsync
, fs.writeFileAsync
now! You can also change the Async
postfix if you are not satisfied.
You can then change your code from
fs.readFile("/path/to/somefile", (err, data) => {
// ...
})
to:
fs.readFileAsync("/path/to/somefile")
.then(/*...*/)
.catch(/*...*/)
//or
try {
const data = await fs.readFileAsync("/path/to/somefile");
// ...
} catch (e) {
// ...
}
If you are using Node version >= 8, you can have promisify from core module util
. See util.promisify.
Developer-friendly#
How is bluebird more developer-friendly than others? To explain this, I strongly encourage you to read this excellent blog post: We have a problem with promises.
Plug into other libraries#
AWS SDK#
const AWS = require("aws-sdk");
const Promise = require("bluebird");
AWS.config.setPromisesDependency(Promise);
IOREDIS#
const Redis = require("ioredis");
const Promise = require("bluebird");
Redis.Promise = Promise;
In-place Q replacement#
If you are using Q 1.x as promise library and still want to give bluebird a try, check bluebird-q
.