Promises – an alternative way to approach asynchronous JavaScript

Even if you’ve done very little JavaScript, you should be familiar with callbacks, which are used heavily for managing asynchronous operations in JavaScript. We’re going to look at an alternative way to handle such asychronous code by using Promises. The examples I’m going to cover are going to use Node.js, but the techniques and libraries involved work equally well for client side JavaScript too. And, you definitely don’t need be an expert on Node.js to follow this article.

Why (not) callbacks?

Let’s get started with a simple example of reading the contents of a file in Node.js:

// readFileExample.js
var FS = require('fs');

FS.readFile('file.txt', 'utf8', function(err, data) {
    if (err) throw err;     
    console.log('File has been read:', data);
});

console.log('After readFile.');

Here we give readFile a function to call (which we refer to as a “callback”). This callback function is invoked once the contents of the file has been read. This is an asynchronous operation because the readFile call is non-blocking and if you were to execute the above program, you will notice that After readFile. will be printed before File has been read.

$ node readFileExample.js
After readFile.
File has been read.

There is nothing wrong about the use of a callback in this example. It’s readable, simple and gets the job done. However, managing callbacks tends to get tricky when you want to do something more complex. I’m going to extend the above example to show what I mean. Let’s say we need to send the file’s data to a couple of web services concurrently and then show the results after both calls finish. Here’s how that will look:

var FS = require('fs'),
    request = require('request');

function getResults(pathToFile, callback) {
    FS.readFile(pathToFile, 'utf8', function(err, data) {
        if (err) return callback(err);
        var response1, response2;

        request.post('http://service1.example.com?data=' + data), function(err, response, body) {
            if(err) return callback(err);
            response1 = response;
            next();
        }); 

        request.post('http://service2.example.com?data=' + data), function(err, response, body) {
            if(err) return callback(err);
            response2 = response;
            next();
        });         

        function next(){
            if(response1 && response2){
                callback(null, [response1, response2]);
            }
        }
    });
}

Do you see what I mean? We had to define a couple of variables (response1 and response2) to track the state of the two requests. The callbacks to both the service calls have to call the next() function, which will then check the state of the requests and print the combined results if both requests are done. The above code is not even complete! For instance, notice that we will be invoking the callback twice if both the service calls fail.

Callbacks also lead to another problem, which you should be already familiar with: callback hell. When you have to perform a number of actions in a specific sequence, nesting your callbacks leads to code like this:

asyncCall(function(err, data1){
    if(err) return callback(err);       
    anotherAsyncCall(function(err2, data2){
        if(err2) return calllback(err2);
        oneMoreAsyncCall(function(err3, data3){
            if(err3) return callback(err3);
            // are we done yet?
        });
    });
});

Let’s try that again with Promises

There are ways to make those callbacks look prettier, but that’s not the point of this article. Instead, I want to show you an alternate way of handling such tricky asynchronous operations, through the use of Promises.

A Promise is a placeholder object that represents the result of an async operation. This object will hold the information about the status of the async operation and will notify us when the async operation succeeds or fails. Enough theory, let’s see re-write the nested callback example above using Promises.

asyncCall()
.then(function(data1){
    // do something...
    return anotherAsyncCall();
})
.then(function(data2){
    // do something...  
    return oneMoreAsyncCall();    
})
.then(function(data3){
   // the third and final async response
})
.fail(function(err) {
   // handle any error resulting from any of the above calls    
})
.done();

The asyncCall(), instead of requiring a callback, returns us a Promise object. The subsequent then() calls on the Promise object also return promises, thus allowing us to chain a sequence of asynchronous operations. The fail() takes a function that will be invoked when any of the preceding asynchronous calls fail. Since an error automatically cascades down to a separate handler, we don’t have to check for them in each stage, like we have to do in the callback-based approach. As you can see, this type of chaining “flattens” the code and drastically improves readability.

Q – a neat library for using Promises

With that rather large introduction to Promises out of the way, let’s look at some practical ways of using Promises when working with Node.js. The first challenge you will face in adopting promises is that all of Node’s core libraries and most of the user-land libraries work using callbacks. Thankfully, there are some awesome libraries that allow us to convert these callback-based APIs to promises. I’m going to use the Q promise library to show how to get going with Promises in Node.js.

Let’s go back to the very first file reading example we saw and re-write that using Q:

// readFileUsingPromises.js
var FS = require('fs'),
    Q = require('q');

Q.nfcall(FS.readFile, "file.txt", "utf-8")
.then(function(data) {      
    console.log('File has been read:', data);
})
.fail(function(err) {
    console.error('Error received:', err);
})
.done();

Q has a handy nfcall() utility function that converts ‘readFile()’ to a promise. With this simple wrapper, you can start using Promises even if a library uses callbacks. Go ahead and try executing the above code (make sure you have the Q module installed by running npm install q first).

Q also makes it very easy to run async operations concurrently. Let’s re-write the earlier example involving service calls using Q. This time, I’m going to choose a real-world API, so you can try executing this code on your own. A file contains the name of a Github repository in a single line, like this:

// repos.txt
joyent/github

We want to read the name of a repository from a file and then call Github’s APIs to get the information about the repository’s collaborators and also fetch the latest commits from the repository. Here’s how that will look:

var FS = require('fs'),
    Q = require('q'),
    request = require('request');

function getResults(pathToFile) {   
    return Q.nfcall(FS.readFile, pathToFile, "utf-8")
    .then(function(repo) {
        var options = { headers: {'User-Agent': 'MyAgent'} }; // github requires user agent string
        return [Q.nfcall(request, 'https://api.github.com/repos/'+repo+'/collaborators', options),
                Q.nfcall(request, 'https://api.github.com/repos/'+repo+'/commits', options)];
    })
    .spread(function(collaboratorsRes, commitsRes) {        
        return [collaboratorsRes[1], commitsRes[1]];  // return the response body
    })
    .fail(function(err) {
        console.error(err)
        return err;
    });
}

// actual call
getResults('repos.txt').then(function(responses) {
    // do something with the responses
});

This example builds on top of the basic constructs of Promises that we have already seen, except for the use of the spread function. Notice how we make 2 concurrent API calls for fetching the collaborators and commits independently. These two calls are returned as an array of promises, which are then “spread” over their eventually fulfilled results. Slick isn’t it? We don’t have to track the state of the individual requests anymore, as we did with the callback based version.

What’s next?

With that, we have covered the basics of Promises and hopefully you got a glimpse of the powerful abstractions they provide us. You should definitely consider using them whenever you’re having trouble managing multiple, inter-dependent asynchronous calls. You will find that Promises definitely makes asynchronous operations easier to reason about. If you want to read more about Q, this guide will be really helpful.

Go ahead, try it out and let me know how it goes!

Back to the articles