Keep your promises with Bluebird

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:

1
2
3
$ 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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:

1
2
3
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:

1
2
3
4
5
(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:

1
2
3
4
5
6
7
8
(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.

1
2
3
4
5
6
7
8
(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:

1
2
3
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:

1
2
3
4
5
6
7
8
(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?

1
2
3
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.

1
2
3
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?

1
2
3
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:

1
2
3
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:

1
2
Promise.resolve([ 1, 2 ,3 ,4 ,5 ])
  .then(arr => arr.sort((a, b) => b - a));

While can be done with:

1
2
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

1
2
3
(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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(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:

1
2
3
4
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:

1
2
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

1
2
3
fs.readFile("/path/to/somefile", (err, data) => {
  // ...
})

to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

1
2
3
const AWS = require("aws-sdk");
const Promise = require("bluebird");
AWS.config.setPromisesDependency(Promise);

IOREDIS

1
2
3
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.

0%