Are there still reasons to use promise libraries like Q or BlueBird now that we have ES6 promises?

Murat Ozgul picture Murat Ozgul · Jan 23, 2016 · Viewed 60k times · Source

After Node.js added native support for promises, are there still reasons to use libraries like Q or BlueBird?

For example if you are starting a new project and let's assume in this project you don't have any dependencies that use these libraries, can we say that there are really no more reasons to use such libraries?

Answer

jfriend00 picture jfriend00 · Jan 23, 2016

The old adage goes that you should pick the right tool for the job. ES6 promises provide the basics. If all you ever want or need is the basics, then that should/could work just fine for you. But, there are more tools in the tool bin than just the basics and there are situations where those additional tools are very useful. And, I'd argue that ES6 promises are even missing some of the basics like promisification that are useful in pretty much every node.js project.

I'm most familiar with the Bluebird promise library so I'll speak mostly from my experience with that library.

So, here are my top 6 reasons to use a more capable Promise library

  1. Non-Promisified async interfaces - .promisify() and .promisifyAll() are incredibly useful to handle all those async interfaces that still require plain callbacks and don't yet return promises - one line of code creates a promisified version of an entire interface.

  2. Faster - Bluebird is significantly faster than native promises in most environments.

  3. Sequencing of async array iteration - Promise.mapSeries() or Promise.reduce() allow you to iterate through an array, calling an async operation on each element, but sequencing the async operations so they happen one after another, not all at the same time. You can do this either because the destination server requires it or because you need to pass one result to the next.

  4. Polyfill - If you want to use promises in older versions of browser clients, you will need a polyfill anyway. May as well get a capable polyfill. Since node.js has ES6 promises, you don't need a polyfill in node.js, but you may in a browser. If you're coding both node.js server and client, it may be very useful to have the same promise library and features in both (easier to share code, context switch between environments, use common coding techniques for async code, etc...).

  5. Other Useful Features - Bluebird has Promise.map(), Promise.some(), Promise.any(), Promise.filter(), Promise.each() and Promise.props() all of which are occasionally handy. While these operations can be performed with ES6 promises and additional code, Bluebird comes with these operations already pre-built and pre-tested so it's simpler and less code to use them.

  6. Built in Warnings and Full Stack Traces - Bluebird has a number of built in warnings that alert you to issues that are probably wrong code or a bug. For example, if you call a function that creates a new promise inside a .then() handler without returning that promise (to link it into the current promise chain), then in most cases, that is an accidental bug and Bluebird will give you a warning to that effect. Other built-in Bluebird warnings are described here.

Here's some more detail on these various topics:

PromisifyAll

In any node.js project, I immediately use Bluebird everywhere because I use .promisifyAll() a lot on standard node.js modules like the fs module.

Node.js does not itself provide a promise interface to the built-in modules that do async IO like the fs module. So, if you want to use promises with those interfaces you are left to either hand code a promise wrapper around each module function you use or get a library that can do that for you or not use promises.

Bluebird's Promise.promisify() and Promise.promisifyAll() provide an automatic wrapping of node.js calling convention async APIs to return promises. It's extremely useful and time saving. I use it all the time.

Here's an example of how that works:

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

fs.readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

The alternative would be to manually create your own promise wrapper for each fs API you wanted to use:

const fs = require('fs');

function readFileAsync(file, options) {
    return new Promise(function(resolve, reject) {
        fs.readFile(file, options, function(err, data) {
            if (err) {
                reject(err);
            } else {
                 resolve(data);
            }
        });
    });
}

readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

And, you have to manually do this for each API function you want to use. This clearly doesn't make sense. It's boilerplate code. You might as well get a utility that does this work for you. Bluebird's Promise.promisify() and Promise.promisifyAll() are such a utility.

Other Useful Features

Here are some of the Bluebird features that I specifically find useful (there are a couple code examples below on how these can save code or speed development):

Promise.promisify()
Promise.promisifyAll()
Promise.map()
Promise.reduce()
Promise.mapSeries()
Promise.delay()

In addition to its useful function, Promise.map() also supports a concurrency option that lets you specify how many operations should be allowed to be running at the same time which is particularly useful when you have a lot of something to do, but can't overwhelm some outside resource.

Some of these can be both called stand-alone and used on a promise that itself resolves to an iterable which can save a lot of code.


Polyfill

In a browser project, since you generally want to still support some browsers that don't have Promise support, you end up needing a polyfill anyway. If you're also using jQuery, you can sometimes just use the promise support built into jQuery (though it is painfully non-standard in some ways, perhaps fixed in jQuery 3.0), but if the project involves any signficant async activity, I find the extended features in Bluebird very useful.


Faster

Also worth noting that Bluebird's promises appear to be significantly faster than the promises built into V8. See this post for more discussion on that topic.


A Big Thing Node.js is Missing

What would make me consider using Bluebird less in node.js development would be if node.js built in a promisify function so you could do something like this:

const fs = requirep('fs');

fs.readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

Or just offer already promisified methods as part of the built-in modules.

Until then, I do this with Bluebird:

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

fs.readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

It seems a bit odd to have ES6 promise support built into node.js and have none of the built-in modules return promises. This needs to get sorted out in node.js. Until then, I use Bluebird to promisify whole libraries. So, it feels like promises are about 20% implemented in node.js now since none of the built-in modules let you use promises with them without manually wrapping them first.


Examples

Here's an example of plain Promises vs. Bluebird's promisify and Promise.map() for reading a set of files in parallel and notifying when done with all the data:

Plain Promises

const files = ["file1.txt", "fileA.txt", "fileB.txt"];
const fs = require('fs');

// make promise version of fs.readFile()
function fsReadFileP(file, options) {
    return new Promise(function(resolve, reject) {
        fs.readFile(file, options, function(err, data) {
            if (err) return reject(err);
            resolve(data);
        });
    });
}


Promise.all(files.map(fsReadFileP)).then(function(results) {
    // files data in results Array
}, function(err) {
    // error here
});

Bluebird Promise.map() and Promise.promisifyAll()

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const files = ["file1.txt", "fileA.txt", "fileB.txt"];

Promise.map(files, fs.readFileAsync).then(function(results) {
    // files data in results Array
}, function(err) {
    // error here
});

Here's an example of plain Promises vs. Bluebird's promisify and Promise.map() when reading a bunch of URLs from a remote host where you can read at most 4 at a time, but want to keep as many requests in parallel as allowed:

Plain JS Promises

const request = require('request');
const urls = [url1, url2, url3, url4, url5, ....];

// make promisified version of request.get()
function requestGetP(url) {
    return new Promise(function(resolve, reject) {
        request.get(url, function(err, data) {
            if (err) return reject(err);
            resolve(data);
        });
    });
}

function getURLs(urlArray, concurrentLimit) {
    var numInFlight = 0;
    var index = 0;
    var results = new Array(urlArray.length);
    return new Promise(function(resolve, reject) {
        function next() {
            // load more until concurrentLimit is reached or until we got to the last one
            while (numInFlight < concurrentLimit && index < urlArray.length) {
                (function(i) {
                    requestGetP(urlArray[index++]).then(function(data) {
                        --numInFlight;
                        results[i] = data;
                        next();
                    }, function(err) {
                        reject(err);
                    });
                    ++numInFlight;
                })(index);
            }
            // since we always call next() upon completion of a request, we can test here
            // to see if there was nothing left to do or finish
            if (numInFlight === 0 && index === urlArray.length) {
                resolve(results);
            }
        }
        next();
    });
}

Bluebird Promises

const Promise = require('bluebird');
const request = Promise.promisifyAll(require('request'));
const urls = [url1, url2, url3, url4, url5, ....];

Promise.map(urls, request.getAsync, {concurrency: 4}).then(function(results) {
    // urls fetched in order in results Array
}, function(err) {
    // error here
});