Building a Node.js module with promises (and not breaking them)

.then against a dark background

In some of my spare time (which I never seem to have enough of these days), I maintain and contribute to the Node.js SDK for the Bandwidth App Platform. Since taking over maintaining it last year, I have been working on a 2.0 rewrite, and have been chipping away at it here and there.

One of the big requests from other developers around the office was built-in support for Promises, so they didn’t have to wrap function calls themselves. At first, I was hesitant. I didn’t want to add yet another dependency to the SDK, and most of all, I didn’t want to force developers down a certain path.

They may already have a favorite Promise library that they are already using in their code. Or they may not want to use Promises at all! As it turns out, having Promise support built into the SDK directly is now one of my favorite features of the upcoming version.

Promises are nice

If you aren’t familiar with Promises in general, it may be helpful to read up on them. They are pretty awesome, and help solve problems like the notorious pyramid of doom:

doTheFirstThing(function (err, firstResult) {
    doTheSecondThing(firstResult, function (err, secondResult) {
        doTheThirdThing(secondResult, function (err, thirdResult) {
            doTheFourthThing(thirdResult);
        });
    });
});

Instead, you could write the above code like this with Promises:

doTheFirstThing()
    .then(doTheSecondThing)
    .then(doTheThirdThing)
    .then(doTheFourthThing);

Nice huh? It’s a lot more readable. Although Promises have their own set of pitfalls if you’re not careful. The nice thing is that most Promise libraries behave the same way, to a certain extent. Any library that implements the Promises/A+ spec will share a lot in common, making them mostly interchangeable.

Choosing a library (or no library)

For the node-bandwidth SDK, we decided to go with the bluebird promise library. Initially, I was hesitant to add extra things to the SDK that were not built into Node itself. Knowing that Promises are a part of ES6, I thought, “Let’s just use native promises and that will be great”. Except for everybody who doesn’t want to use ES6 yet.

For someone who already has a sophisticated application, it may be a lot of effort (due to other dependency compatibility issues) to move to a newer version of Node with ES6 support. This would leave these developers out in the cold.

Bluebird solves this by adding Promise support to earlier versions of ECMAScript. But I still thought “Surely having an extra compatibility layer can’t be as good as using native objects.” As it turns out, though it really surprised me at first, bluebird Promises are actually faster, and more memory efficient than native ES6 Promises.

For a good explanation why, see this post by the author of bluebird. Because of its support for older versions of Node, and its sheer speed and optimization, we went with bluebird.

But what about callbacks?

As someone who has been using Promises for a long time now, it does seem strange that someone would still want to stick with callback style programming for everything. However, there are some times where I still find myself wanting to do something super simple, and using a Promise doesn’t really add any significant value. I still didn’t want to force users of the SDK to use Promises, when they may actually want to use callbacks though. Once again, bluebird to the rescue!

.asCallback()

Bluebird has a wonderful function that you can call on a Promise named asCallback()which, as you might suspect, takes a Promise and lets you call a regular Node style (err, result) callback function (in previous versions of bluebird this function was called nodeify()). The function takes a single argument, the pointer to the callback function.

In my module’s code, I prefer to use Promises internally. On public API functions though, I allow for an optional callback argument, and return a Promise. This looks something like:

var someOtherModule = require('./someOtherModule');

var myModule = function() {
    this.doSomethingCool(input, callback) {
        // do something cool here
        return someOtherModule.thatReturnsPromises(input)
        .then(function (result) {
            // do something else cool here
            return result++;
        })
        .asCallback(callback);
    }
}

module.exports = myModule;

Ultimate flexibility

This allows end users of your module (because you’re going to publish this on GitHub and npm, right?) the utmost flexibility. They can call your function with a traditional Node style callback function:

var myModule = require('myModule');

myModule.doSomethingCool(100, function (err, result) {
    if (err) {
        console.error(err);
        return;
    }
    console.log(result);
});

Or they could use the Promise that is returned by your function:

var myModule = require('myModule');

myModule.doSomethingCool(100)
.then(function (result) {
    console.log(result);
})
.catch(function (err) {
    console.error(err);
});

I much prefer the second option, but a lot of developers find Promises confusing, especially if they are new to Node.js, so they may be more comfortable with callbacks still. This should make everybody happy ?

What about error handling?

It’s free! As in free speech. If the Promise was rejected at any point, err will be set, and result will not be set. If the Promise is not rejected, but is fulfilled, result will be set, and err will be null, just like you’d expect from a normal Node function.

My example above is a little simplified because I’m assuming that the other function that I’m calling is already going to return a Promise. There aren’t too many modules out there that will do that out of the box.

However, you could use something like bluebird’s promisify() function to wrap another module. That’s actually what I ended up doing for the popular request module, so that I could get Promises for REST API calls. It’s great!

In conclusion…

If you ever find yourself working on a cool Node.js module that you hope to share with the world someday, maybe this will be helpful to you. If not, at least you now know about the awesome bluebird library. Promises are great. Especially when they are kept.