Asynchronous JavaScript: The Bad, the Ugly ( callbacks chaining ), the Good ( promises chaining ) and parallel operations

Although is was published several months ago, in the past few days I received this post: 3 formas de manejar la asincronía en JavaScript via newsletter from Carlos Azaustre, it is an excelent introduction to asynchronous JavaScript.

We are suposed to be familiar with Node.js, and particularly with its asynchronous nature.

In the above post, Carlos shares with us three orientations to manage asynchronous calls. We are assuming that we already know how to invoke a function that execute an asynchronous operation, and then execute some code when the function finishes its execution. But what happens when there are several asynchronous functions to call, and some code must be executed when all theese functions have finished?

We've prepared some examples with different approaches, and we're comparing them.

Note: Requester.getResponseCode is a helper we've coded, that relies on request to make http calls.

(you can get sources from my Git repository

Chaining calls to asynchronous functions

One idea is to nest (chain) calls to functions, and as Carlos show us in the post, it can be done in several ways.

The Bad: it does not work, so... avoid it.

    var bingResult = false;
    var googleResult = false;

    console.log('Starting Bing');
    requester.getResponseCode('http://www.bing.es',
            function(statusCode) {
                var result = (statusCode == 200);
                bingResult = result;
                console.log('bing: '+statusCode);
            }
    );
    console.log('Starting Google');
    requester.getResponseCode('http://www.google.es',
            function(statusCode) {
                var result = (statusCode == 200);
                googleResult = result;
                console.log('google: '+statusCode);
            }
    );      
    console.log('bing: '+bingResult);   
    console.log('google: '+googleResult);

    console.log('Finished!');
    res.end('Finished!');

It's very clear that this orientation doesn't work. As we can see in the log:

Starting Bing

Starting Google

bing: false

google: false

Finished!

google: 200

bing: 200

the funtions are called but due to its asynchronicity, execution continues without wait funtions to finish. So the final ("Finished!") message appears before function's execution messages.

The Ugly: Callbacks Chaining (the first one that works, in the post below, it's called "Callbacks").

The second function is invoked inside the callback of the first function. Easy. Clear.

var bingResult = false;
var googleResult = false;
console.log('Starting Bing');
requester.getResponseCode('http://www.bing.es',
    function(statusCode) {
        var result = (statusCode == 200);
        bingResult = result;
        console.log('bing: '+statusCode);

        console.log('Starting Google');            
        requester.getResponseCode('http://www.google.es',
            function(statusCode) {
                var result = (statusCode == 200);                    
                googleResult = result;
                console.log('google: '+statusCode);

                console.log('bing: '+bingResult);                                
                console.log('google: '+googleResult);
                console.log('Finished!');
                res.end('Finished!');    
            }
        );                            
    }
);

As we can see in the log:

Starting Bing

bing: 200

Starting Google

google: 200

bing: true

google: true

Finished!

the functions callback messages appear first, then (after all) final message does.

The good: Promises Chaining (the most popular actually, in the post below, it's called "Promises").

This code can be considered "PRO" ;-) It does work, its clear and maintainable. Promises and first created, second chained using .then().

    var bingResult = false;
    var googleResult = false;

    var promise1 = function() {
        return new Promise(function(resolver, cancelar) {

            console.log('Starting Bing');
            requester.getResponseCode('http://www.bing.es',
                    function(statusCode) {
                        var result = (statusCode == 200);
                        bingResult = result;
                        console.log('bing: '+statusCode);
                        resolver();                         
                    }
            );

        });
    }

    var promise2 = function() {
        return new Promise(function(resolver, cancelar) {

            console.log('Starting Google');
            requester.getResponseCode('http://www.googleg.es',
                    function(statusCode) {
                        var result = (statusCode == 200);
                        googleResult = result;
                        console.log('google: '+statusCode);
                        resolver();
                    }
            );

        });
    }

    res.writeHead(200, {'Content-Type': 'text/html'});

    promise1().then(promise2).then(function() {
        console.log('bing: '+bingResult);   
        console.log('google: '+googleResult);
        console.log('Finished!');
        res.end('Finished!');
    });

As we can see in the log:

Starting Bing

bing: 200

Starting Google

google: 200

bing: true

google: true

Finished!

It works properly. Second function invocation only occurring after the first one completed. And of course, after all, the final message appears.

The fancy way: Async/Await (state-of-the-art technology)

Async functions have been accepted into the ES7 standards. We're not talking about it now, if you want to know more about it, you can pay attention to Carlos introduction, it seems to be a very good one.

Parallel operations: If you need to run multiple tasks that doesn’t depend on each other and when they all finish do something else, you should run it in parallel.

If this operations are such as file I/O, querying DB or networking. If your tasks contain this kind of calls, they'll appear as if the've been processed in parallel. If not, they will actually be executed in series.

How can this be possible? I/O tasks spent most of its processing time waiting for the result of the I/O call. Node.js starts processing the first task until it pauses to do an I/O call. At the moment, Node.js leaves it and grants its main thread to another task.

Remember that in single-threaded event loops you can never do more than one thing at once. But you can wait for many things at once just fine.

There are severals ways to code parallel operations in JavaScript, we've prepared a couple of examples.

Parallel operations with Callbacks using async

Async is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript. Although originally designed for use with Node.js and installable via npm install async, it can also be used directly in the browser.

Async provides around 20 functions that include the usual 'functional' suspects, as well as some common patterns for asynchronous control flow (parallel, series, waterfall…). In our example we're using one of theese functions (async.parallel).

parallel(tasks, [callback])

Run the tasks array of functions in parallel, without waiting until the previous function has completed. Note: parallel starts I/O tasks in parallel, it doesn't make the "parallel execution of code dream" real.

    async.parallel({
        bingRequest: function(callback) {
            console.log('Starting Bing');
            requester.getResponseCode('http://www.bing.es',
                    function(statusCode) {
                        var result = (statusCode == 200);
                        console.log('bing: '+statusCode);
                        callback(null, result);
                    }
            );

        },
        googleRequest: function(callback) {
            console.log('Starting Google');
            requester.getResponseCode('http://www.google.es',
                    function(statusCode) {
                        var result = (statusCode == 200);
                        console.log('google: '+statusCode);
                        callback(null, result);
                    }
            );

        }
    }, function(err, results) {
        var bingResult = results.bingRequest;
        var googleResult = results.googleRequest;
        console.log('bing: '+bingResult);   
        console.log('google: '+bingResult);
        console.log('Finished!');
        res.end('Finished!');
    })      

As we can see in the log

Starting Bing

Starting Google

google: 200

bing: 200

bing: true

google: true

Finished!

Execution doesn't wait for the first function to finish to start the second.

Parallel operations with Promise.all

Promise.all allows you run multiple asynchronous operations at once and continue on your way once all of them have completed. The Promise.all method takes an array of promises and fires one callback once they are all resolved.

    var promise1 = function() {
        return new Promise(function(resolver, cancelar) {

            console.log('Starting Bing');
            requester.getResponseCode('http://www.bing.es',
                    function(statusCode) {
                        var result = (statusCode == 200);
                        console.log('bing: '+statusCode);
                        resolver(result);                           
                    }
            );

        });
    }

    var promise2 = function() {
        return new Promise(function(resolver, cancelar) {

            console.log('Starting Google');
            requester.getResponseCode('http://www.googleg.es',
                    function(statusCode) {
                        var result = (statusCode == 200);
                        console.log('google: '+statusCode);
                        resolver(result);
                    }
            );

        });
    };

    Promise.all([promise1(), promise2()]).then(function(results) {
        var bingResult = results[0];
        var googleResult = results[1];
        console.log('bing: ' + bingResult); 
        console.log('google: ' + googleResult);

        console.log('Finished!');
        res.end('Finished!');
    });

Benchmarking

We've prepared a sort of time comsumption comparative between different approaches. Each piece of code consumes four popular URL's (bing.es, google.es, nasa.gov and loc.gov), and after finishing, they show the total time in miliseconds for finishing the four request and retrieving the four responses. Each test have run ten times, taked noted of the miliseconds logged, and calculated min. execution time, max execution time and average execution time. If you are interested about result please take a look at the table below:

 Promises
chaining
Parallel
with async
Parallel
with Promises.all
Min.1141359345
Max.3037807866
Avg.1730553477

Conclussion:

We would typically not use series approach because is demonstrably slower than using a parallel approach. Promise.all (as well as asyc.parallel does) is firing off multiple asynchronous operations at one time, to reduce the total execution time. Again, thist post is isn't just about Node.js, or JavaScript. It is about asynchronous programming in general, and its different approaches.