Using Promises in your Asynchronous Code

Eddie Flores
February 24, 2015

A Case for Promises

If you’ve written your share of JavaScript, you’ve likely encountered scenarios where independent asynchronous requests need to complete before execution can continue. In many cases, you’re forced to impose an order to the requests and nest some callbacks to be notified of their completion. Another common, albeit dirty approach, is to pollute the parent scope of the events with flags of some sort and poll them on a particular time interval until completion.

In either case, maintainability becomes a nightmare and will likely cause future readers of your code to do an enormous amount of desk flipping.

Fortunately enough, we can dig into our bag of constructs and make use of what’s commonly referred to as a promise, an abstract concept thats made its way into jQuery and has recently gained native support in modern browsers.

Promises - A Brief Introduction

A promise is an object that represents an asynchronous computation that is not yet known or complete. Its main feature is its ability to encapsulate the states of an asynchronous computation and expose them through a callback registering mechanism. By definition, a promise is always in one of three states: pending, fulfilled, or rejected. The naming of those states typically varies throughout different implementations, but the general gist of what a promise represents is consistent. In the following code samples, we’ll be using jQuery’s promise object to illustrate its usage.

Here’s some code to go with that definition:

var operation = startMyAsyncOperation(); // Pending
operation.done(handleSuccess);           // Fulfilled
operation.fail(handleFailure);           // Rejected

Alright, great, so we’re registering callbacks instead of passing them along. What’s the big deal? Well, now you’re able to separate concerns a bit better, typically a callback based solution would look like this:

var handleCompletion = function (data, err) {
    if (err) {
        handleFailure(err);             // Rejected
    } else {
        handleSucess(data);             // Fulfilled
    }
};
startMyAsyncOperation(handleCompletion); // Pending

The main take away here is not that you’re writing less code, which is nice, but rather that you’re locked into handling success or failure via a single callback. In practice, your handling of data might require that you trigger an animation, generate new DOM elements, fire analytics events, remove old DOM elements, the list goes on. The current approach would require that you do all of those things within a single block of code. Promises allow you to stack handlers while improving readability:

var operation = startMyAsyncOperation();        // Pending
operation.done(hideLoadingAnimation);           // Fulfilled
operation.done(trackViewEvent);                 //    "
opeartion.done(hideOutdatedElements);           //    "
operation.done(renderDetails);                  //    "

Best of all, jQuery provides a $.when method that allows you to combine multiple promises and be notified when all of them have completed.

var promise1 = startAsyncOperation1();
var promise2 = startAsyncOperation2();
var promise3 = startAsyncOperation3();

var promises = $.when(promise1, promise2, promise3);

promises.done(function (data1, data2, data3) {
    // All of your promises are done!
});

promises.fail(function (err) {
    // One of your promises failed, do something...
});

And that’s a perfect segue to an actual example!

Dealing with Independent Async Operations

Imagine the following scenario: You have a service-oriented application that provides authenticated users with their browsing history, stats by page, and a page summary. The problem centers around independent XHR calls that retrieve data from different endpoints. Since they’re independent requests, you’ll have to do some code jumbling to determine when the XHR calls have completed before you can do something with their results.

Using callbacks:

// You smell that?

var getPageDetails = function (page, cb) {
    $.get({
        url: '/page/' + page.id,
        success: function (summary) {
            cb(summary, null);
        },
        fail: function (err) {
            cb(null, err);
        }
    });
};

var getPageStats = function (page, cb) {
    $.get({
        url: '/page/stats/' + page.id,
        success: function (stats) {
            cb(stats, null);
        },
        fail: function (err) {
            cb(null, err);
        }
    })
};

// Like pyramids? ...of doom?

var retrieveAndRenderPages = function (pages) {
    var data = [];
    pages.forEach(function (page) {
        getPageDetails(page, function (details, detailsErr) {
            if (!detailsErr) {
                getPageStats(page, function (stats, statsErr) {
                    if (!statsErr) {
                        renderPage(details, stats);
                    }  else { /* Do something with the error */}
                });
            } else {
                // Do something with the error
            }
        });
    });
};

var login = function (credentials) {
    $.post({
        url: '/login',
        data: credentials,
        success: function (pages) {
            retrieveAndRenderPages(pages);
        }
    });
};

$('#login-form').on('submit', function (evt) {
    evt.preventDefault();
    var credentials = $(this).serialize();
    login(credentials);
});

By the way, note that getPageDetails() and getPageStats() are meant to be used independently and have no reliance on each other. Despite this, we were forced to impose an order of execution within retrieveAndRenderPages() to have an execution path for our nested callbacks.

That’s a whole lot of maneuvering and contortion we can do away with by using promises.

Promise, we can do better…

// Time to freshen up...

var getPageDetails = function (page) {
    return $.get('/page/' + page.id);
};

var getPageStats = function (page) {
    return $.get('/page/details/' + page.id);
};

var login = function (credentials) {
    return $.post('/login', credentials);
};

// Clean.

var retrieveAndRenderPages = function (pages) {
    pages.forEach(function (page) {
        var detailsXHR = getPageDetails(page),
            statsXHR = getPageStats(page),
            both = $.when(detailsXHR, statsXHR);
        both.done(function (details, stats) {
            renderPage(details, stats);
        });
        both.fail(function (err) {
            // Handle the error.
        });
    });
};

$('#login-form').on('submit', function (evt) {
    evt.preventDefault();
    var credentials = $(this).serialize();
    var loginXHR = login(credentials);
    loginXHR.done(function (user) {
        retrieveAndRenderPages(user.pages);
    });
});

Isn’t that more readable? Notice how we used $.when() to group all promises into one. In the process we’ve flattened our code and removed some of the complexity involved with following callback paths.

Final Thoughts

Promises are a great way to write sane asynchronous code. As with everything else in life, use them when the time is right. In most cases, simple code trumps overly engineered code. For more on the subject, these are great reads: