Common patterns when handling promises

Promises are becoming a top feature of JavaScript. They’re already part of most libraries and frameworks (jQuery, WinJS and AngularJS among others) and will soon be included natively in all browsers with the upcoming delivery of ECMAScript 6 (the standard behind JavaScript). They are heavily used every time you have to deal with asynchronous calls, like in Ajax, or in Single Page Applications which rely a lot on them.

I’ve been using them proficiently for different projects, and I noticed there are common patterns that keep popping in my code, and are not necessarily well known for the time being. I propose we take a look at three of them.

1. Looping on an array of items

The first pattern is pretty principle. You have an array of items, and you must loop on them and make an asynchronous call for each. But your code can only continue once all those calls are resolved, because you need the information they will pull back.

var items = [ /* whatever... */ ];

items.forEach(function (item) {
    callToFunctionAsync(item).then(function(result) {
        item.someProperty = result;
    });
});

// I want to continue once all the async calls are resolved

This one is quite easy to tackle for those who have already mastered the basics of promises. All the APIs contain a special method that allows you to aggregate promises to create some sort of master promise, that resolves once every single one of its parents have resolved themselves. So here is what it should look like once you have completed the code.

var items = [ /* whatever... */ ];

var promises = [];
items.forEach(function (item) {
    var promise = callToFunctionAsync(item).then(function(result) {
        item.someProperty = result;
    });
    promises.push(promise);
});

// With jQuery
$.when(promises).done(function() {
    // Continue...
});

// With WinJS
WinJS.Promise.join(promises).done(function() {
    // Continue...
});

// With AngularJS
$q.all(promises).done(function() {
    // Continue...
});

Actually, this is such a common pattern that I suggest you to create an utility method to manage it more easily and make it less verbose. Here is an example written for jQuery, WinJS or AngularJS (should be easy to adapt to other libraries).

/* Adding the routine to the 'Array' prototype will make it much easier to use
 * but don't forget to check first it does not already exists.
 * Also, feel free to add it with a different name or as a static method if you feel like it. */
if (!Array.prototype.forEachAsync) {
    Array.prototype.forEachAsync = function(processingAsync) {
        var promises = [];
        this.forEach(function(item) {
            var promise = processingAsync(item);
            promises.push(promise);
        });

        // With jQuery
        return $.when(promises);

        // With WinJS
        return WinJS.Promise.join(promises);

        // With AngularJS
        return $q.all(promises);
    };
}

// To use it
[ /* whatever... */ ].forEachAsync(function(item) {
    // The actual async processing
}).done(function() {
    // Continue...
});

Notice the code is almost the same for all libraries, except for the method used to gather the promises. This is because their implementation of the promises principle are all based on the CommonJS Promise/A design.

2. Looping on an array of items, the cascading style

Our second pattern looks a lot like the first one, but there’s a twist. This time we want the asynchronous calls for each of our items to be made in order, not in parallel like in the first pattern. Which means, they have to « cascade » one after the other. This can be necessary if the asynchronous calls have side effects.

To achieve this result, this time we are not going to create an empty array to gather the promises resulting from the asynchronous calls. Instead we are going to create our own master promise and chain all the asynchronous calls on it. This example is written for WinJS.

var items = [ /* whatever... */ ];

// We create a promise which is already resolved (returning null)
var masterPromise = WinJS.Promise.wrap(null);

items.forEach(function (item) {
    // This is where we chain all the asynchronous calls one after the other
    masterPromise = masterPromise.then(function() {
        return callToFunctionAsync(item).then(function(result) {
            item.someProperty = result;
        });
    });
});

masterPromise.done(function() {
    // Continue...
});

To understand this second pattern, you really have to grasp the asynchronous nature of promises, and the use of the ‘then’ method to chain them. Again, what’s really important here is that in the first pattern, the asynchronous calls were made in parallel. Here they are made in order, and one asynchronous call will not start before the previous one has finished.

Note that this routine can also be factorized into an Array.forEachCascadeAsync method, but I’ll leave the implementation to you. It’s a very good exercise to check if you have understood the principle.

3. Mixing synchronous and asynchronous paths

Another very common pattern I had to deal with very often is when a condition makes your code split into different paths, some being purely synchronous and procedural, and others being asynchronous. This is what makes you think a lot about the « three dimensional » nature of promises.

var result;

// We generate a random number between '0' and '10'
var x = Math.floor((Math.random() * 11));

// The code will choose either path randomly depending on the value of 'x'
if (x <= 5) {
    // This path is synchronous
    result = "foo";
} else {
    // This path is asynchronous
    callToFunctionAsync().done(function(resultAsync) {
        result = resultAsync;
    });
}

// Problem : if we took the asynchronous path, the call has not yet resolved and 'result' value is undefined
console.log(result);

There are multiple ways to fix this code, and generally beginners who just started to handle promises have difficulties to find out which one is best to tackle this problem.

The trick is to make all ways to be asynchronous, by wrapping the condition with your own promise that you solely manage. Again I’ll show the example only with WinJS.

var result;

// We generate a random number between '0' and '10'
var x = Math.floor((Math.random() * 11));

// Here we create our own promise to wrap both paths
// jQuery and AngularJS have different ways to create and manage your own promise (jQuery calls it "Deferred object")
new WinJS.Promise(function(complete, error, progress) {
   // The code will choose either path randomly depending on the value of 'x'
    if (x <= 5) {
        // This path is synchronous
        result = "foo";
        complete();
    } else {
        // This path is asynchronous
        // Note that we pass the 'error' callback in order to catch possible exceptions and treat them later
        callToFunctionAsync().done(function(resultAsync) {
            result = resultAsync;
            complete();
        }, error);
    }
}).done(function () {
    // Now 'result' will be set whichever path we took
    console.log(result);
}, function (error) {
    // And you can even manage potential exceptions raised by the asynchronous calls inside the condition
});

Yes this code is a little bit more verbose, but it’s readability is very good (which is IMO very important), and there won’t be any surprise with it, including with possible exceptions being raised. Besides, in this example we could get rid of the ‘result’ variable and have both paths return their own value through the ‘complete’ method (which marks the completion of the promise we manage, in case you did not understand).

If you and all your team mates use this pattern whenever you have to deal with such a problem, it will be very easy to recognize and handle.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s