Promises - Asynchronous processes made easy

Promises - Asynchronous processes made easy

Javascript developers are knowing the struggle - you're having a bunch of asynchronous processes you have to time and use their results to call other methods. If it comes to XML HTTP requests or the FileReader API - asynchronous methods are everywhere in the JavaScript world. Without a sophisticated architecture you're probably ending up in the well-known "pyramid of doom" or "callback hell". You're nesting callbacks over callbacks which has a huge impact of the maintainability and adds a lot of dependencies to your code.

step1(function (value1) {
   step2(value1, function(value2) {
       step3(value2, function(value3) {
           step4(value3, function(value4) {
               // Do something with value4
           });
       });
   });
});

Example for a typical "pyramid of doom"

Promises are a way around this problem. This blog post will take you on a ride to explore how promises are working and how you can use them right away. The goal is to get rid of the "pyramid of doom" and flatten the pyramid like this:

promise.then(step1)
.then(step2)
.then(step3)
.then(step3)
.then(function(value4) {
    // Do something with value4
})
.catch(function(err) {
   // Handle any error from all above steps
});

Example for a promise based approach

Getting started with promises

The technology is part of the ECMAScript 2015 (ES6) standard. The Promise object is used for asynchronous operations and has four states which are representing the current status of the operation:

  • pending - initial status, the promise is not fulfilled or rejected at this point
  • fulfilled - the operation was successful
  • rejected - the operation wasn't successful and an error occurred
  • settled - the operation is not fulfilled or rejected and not pending anymore

The Promise interface represents a proxy for a value which is not defined / known at the time the promise was initialized. This allows an association of a handler method which will be triggered with the success or failure of an asynchronous operation.

A promise with the status "pending" can be fulfilled with a value from the asynchronous operation or rejected with the occurring error message. Let's take a look on the workflow of a promise:

Once a promise is fulfilled or rejected, it is immutable (e.g. it can never change again).

Compatibility

Before we're jumping right into it, I always like to take a look on the compatibility to terminate if it targets our supported browsers and if we can use it today.

All of our target browsers are supported but your environment probably looks different than ours. No worries, there are polyfills out there which can provide you with the missing functionality. Here are a few:

Browser / Node.js:

If you're using jQuery version 1.5 or newer you don't even have to worry about polyfilling promises - it comes with its own implementation but more on this later on.

Constructing a promise

Imagine we're having a function which sends an XML HTTP request using GET to request and receive data from the server.

function getData(url, callback) {
    var req = new XMLHttpRequest();

    req.open('GET', url);

    req.onload = function() {
        if (req.status !== 200) {
            callback(new Error("Status code wasn't 200"), null);
            return;
        }

        callback(null, req.response);
    }

    req.onerror = function() {
        callback(new Error("Network error"), null);
    }

    req.send();
}

// A typical call looks like this
getData('product.json', function(err, result) {
       if (err) {
           console.error('Failed!', err);
           return false;
       }

       console.log('Success!', result)
});

In the above example I'm using a callback based approach to solve the problem to handle an asynchronous operation like requesting data from the server. Before we're transforming this code into a promise I would like to show off the basic syntax of a promise:

new Promise(executor);

// e.g.
new Promise(function(fulfill, reject) { ... });
  • executor

    • The executor is a function with the two arguments fulfill and reject. The first argument computes the promise, the second one discards the promise.

Now we know how a promise works and how the syntax looks like, let's promisify the above example:

// We removed the callback parameter cause we don't need it anymore
function getData(url) {

    // Return a new promise and wrap the previous logic into the anonymous function
    return new Promise(function(fulfill, reject) {
        var req = new XMLHttpRequest();

        req.open('GET', url);

        req.onload = function() {
            if (req.status !== 200) {
                // Instead of the callback we're calling the reject callback of the promise
                reject(new Error("Status code wasn't 200"));
                return;
            }

            // Everything was fine, so we can fulfill the promise
            fulfill(req.response);
        }

        req.onerror = function() {
            // We're rejecting the promise here cause an error occured
            reject(new Error("Network error"));
        }

        req.send();
    });

}

// A typical call looks like this
getData('product.json').then(function(result) {
    console.log('Success!', result)
}, function(err) {
    console.error('Failed!', err);
});

// ...or you can use catch to handle the errors
getData('product.json').then(function(result) {
    console.log('Success!', result)
}).catch(function(err) {
    console.error('Failed!', err);
});

As you can see in the code example it is very easy to remove the callback approach and replace it with a promise. Basically we're wrapping our logic into a new promise and replace the callback calls with a call of either the fulfill or reject method.

Queuing asynchronous operation

You can also chain then() calls to run asynchronous operation in sequence. If you return a promise in a then(), the next then() will be called with the returned promise, which can be used to enable chaining operations:

getData('product.json').then(function(product) {
    return getData(product.productDetailsUrl);
}).then(function(productDetails) {
    console.log('Do something with the product details');
});

It's looks like magic but works wonderful. Error handling can be added by simply adding a catch() call to the function chain. The catch() method will be triggered if one of the promises gets rejected.

Promises with callbacks - best of both worlds with Q

Q is a popular JavaScript Promise library which I personally use in Node.js. Node.js methods are usually asynchronous unless you're using the synchronous version of the method which isn't the best idea. Therefore you're having a bunch of callbacks in your code / module and every third party developers assumes you're working with callbacks as well.

One of the benefits of Q is that you can provide a promise and support the callback approach at the same time. In the following example we're creating a method which concatenates the firstname and lastname of a user:

var Q = require('q');

module.exports = {
    getFullName: function (firstName, lastName, callback) {
        var deferred = Q.defer();

        if (firstName && lastName) {
            var fullName = firstName + " " + lastName;
            deferred.resolve(fullName);
        }
        else {
            deferred.reject("First and last name must be passed.");
        }

        deferred.promise.nodeify(callback);
        return deferred.promise;
    }
}

You can use the promise or the callback whatever floats your boat:

nameModule.getFullName('John', 'Doe').then(function(fullName) {
    console.log(fullName)
}).fail(function(err) {
    console.error(err);
});

...or using a callback approach:

nameModule.getFullName('John', 'Doe', function(err, fullName) {
    if (err) {
        console.error(err);
        return false;
    }

    console.log(fullName);
});

The magic in the above code is the call of deferred.promise.nodeify(callback). It automatically assumes it's a Node.js-style callback and calls it as either callback(err, null) with the provided error when the promise was rejected or callback(null, result) when the promise becomes fulfilled. If callback is not a function, it simply returns the promise.

jQuery - promises and deferred functions

jQuery 1.5 introduced the Deferred object which provides a way to register multiple callbacks into self-managed callback queues, invoke callback queues as appropriate, and relay the success or failure state of any synchronous or asynchronous function. The Promise object is a subset of the methods from the Deferred object and prevents the user from changing the state of the Deferred, so it's immutable.

Enough theory, let's take a look on an actual code example. We'll create timers which will be using promises. Let's take a look on one of the usual ways to solve the problem:

var waitingTimer = function(time, notify, callback) {
    var timer;

    // Make sure our values are defined.
    time = time || 10000;
    callback = callback || function() {};
    notify = notify || function() {};

    timer = window.setInterval(function() {
        notify.apply(null);
    }, 1000);

    window.setTimeout(function() {
        window.clearInterval(timer);
        cb.apply(null);
    }, time);
};

The above code can be used like that:

$('button').on('click', function() {
    var $waitingTimer = $('.waiting-timer');

    waitingTimer(5000, function() {
        $waitingTimer.html($waitingTimer.html() + ".");
      }, function() {
        $waitingTimer.html("Done!");
      });
});

Now let us promisify the above code:

var waitingTimer = function(time) {

    // Get the global deferred object
    var deferred = $.Deferred(),
        timer;

    // Make sure our values are defined.
    time = time || 10000;

    timer = window.setInterval(function() {
        // Fire the notify method on the deferred object
        deferred.notify();
    }, 1000);

    window.setTimeout(function() {
        window.clearInterval(timer);

        // Resolve the promise
        deferred.resolve();
    }, time);

    // We're returning the promise at that point
    return deferred.promise();
};

The promisify code can be used like that:

$('button').on('click', function() {
    var $waitingTimer = $('.waiting-timer');

    waitingTimer(5000).progress(function() {
        $waitingTimer.html($waitingTimer.html() + ".");
      }).done(function() {
        $waitingTimer.html("Done!");
    });
});

As a side note: Quite a few jQuery methods are using promises or the Deferred object already. One of the most common one is $.ajax() which allows to use a promise instead of callback methods:

$.ajax({
    url: "/ServerResource.txt"
}).done(result, function(result) {
    console.log("Success!", result);
}).fail(function(err) {
    console.error('Failed!', err);
});

jQuery uses the CommonJS Promises/A interface for the jQuery XMLHttpRequest.

There's just one downside you have to consider when working with promises or the Deferred object. jQuery's promises are linked to a Deferred object stored on the .data() for an element. Since the .remove() method removes the element's data as well as the element itself, it will prevent any of the element's unresolved Promises from resolving.

Conclusion

Promises are looking like a brand new technology, but apparently they are not. jQuery introduced them with version 1.5 which was released back in 2011. They provide a great and easy to use way to overcome the problem of having nesting callbacks in your application. As we saw in the blog post it's very easy to transform your callback code into a promised based approach which provides a higher flexibility and an easier maintainable code base.

The compatibility is great, there are a lot of polyfills out there and the fact that jQuery comes with an own implementation of promises and deferred functions let me think, that there's no reason not to use promises now.

We went over the basics on what you can do with promises. There are a bunch of different methods in the official standard which allow you to do much more advanced things. We'll cover them in one of the next blog posts.

Back to overview
Top