async tutorial: asynchronous programming in node.js with love JavaScript 04.08.2016

nodejs_async.png

Asynchronous programming is great for faster execution of programs but it comes with price. It's difficult to program and most of the time we end up having callback hell scenario. This happens due to the asynchronous nature of the JavaScript. We want to execute tasks which are dependent on each other hence we wrap them into the callbacks of each function and hence caught into callback hell situation.

To avoid callback hell, follow one or combination of the following

  • modularise your code
  • use generators
  • use promises
  • use event-driven programming
  • use async.js

The async.js library can help us immensely when implementing complex asynchronous control flows, but one difficulty with it is choosing the right helper for the problem at hand.

For example, for the case of the sequential execution flow, there are around 20 different functions to choose from, including eachSeries(), mapSeries(), filterSeries(), rejectSeries(), reduce(), reduceRight(), detectSeries(), concatSeries(), series(), whilst(), doWhilst(), until(), doUntil(), forever(), waterfall(), compose(), seq(), applyEachSeries(), iterator(), and timesSeries().

The async library doesn't lack functions to handle parallel flows; among them we can find each(), map(), filter(), reject(), detect(), some(), every(), concat(), parallel(), applyEach(), and times(). They follow the same logic as the functions we have already seen for sequential execution, with the difference being that the tasks provided are executed in parallel.

If you are wondering if async can also be used to limit the concurrency of parallel tasks, the answer is yes, it can! We have a few functions we can use for that, namely, eachLimit(), mapLimit(), parallelLimit(), queue(), and cargo().

Following is a typical situation that we want to avoid and it’s easy to see how this can get out of control if not properly managed:

function someAsyncTask(taskID, callback) {
    setTimeout(function() {
        console.log('Done async task: ' + taskID);
        callback(null, taskID);
    }, 1000);
}

// calling 5 async tasks one after another, the hellish way
someAsyncTask(1, function() {
    someAsyncTask(2, function() {
        someAsyncTask(3, function() {
            someAsyncTask(4, function() {
                someAsyncTask(5, function() {
                    console.log('All finished.');
                });
            });
        });
    });
});

Let's see most popular scenarios with async.

Scenario 1: run multiple tasks one after another and once they are finish execute something else.

let async = require('async');

async.series([
    function(callback) {
        // some async task
        callback();
    },
    function(callback) {
        // some async task
        callback();
    }
  ],function(err) {
      // Code to execute when everything is done.
});

Example

function finalCallback(err, results) {
    console.log('results:');
    console.log(results); // [1,2,3,4,5]
}

// run these tasks one after the other
async.series([
        function(callback) { someAsyncTask(1, callback); }, 
        function(callback) { someAsyncTask(2, callback); }, 
        function(callback) { someAsyncTask(3, callback); }, 
        function(callback) { someAsyncTask(4, callback); }, 
        function(callback) { someAsyncTask(5, callback); }, 
    ],
    finalCallback
);

Scenario 2: run multiple tasks that does not depend on each other and when they all finish do something else.

let async = require('async');

async.parallel([
    function(callback) {
        // Some Async task
        callback();
    },
    function(callback) {
        // Some Async task
        callback();
    }
  ],function(err,data) {
      // Code to execute when everything is done.
});

Example

async.parallel([
        function(callback) { someAsyncTask(1, callback); }, 
        function(callback) { someAsyncTask(2, callback); }, 
        function(callback) { someAsyncTask(3, callback); }, 
        function(callback) { someAsyncTask(4, callback); }, 
        function(callback) { someAsyncTask(5, callback); }, 
    ],
    finalCallback
); 

Scenario 3: run multiple tasks one after another and exchange data between them and once they are finish execute something else.

This is the scenario very similar to above one except that we need to pass some data to the next function. Async.series() will pass each functions data to final callback function not to the next one.

let async = require('async');

async.waterfall([
    function(callback) {
        // some code to execute
        // in case to go to next function provide callback like this.
        callback(null,valueForNextFunction);
        // Got some error ? Don't wanna go further.
        // Provide true in callback and execution will stop.
        //callback(true,"Some error");
    },
    function(parameterValue,callback) {
        // Some code to execute.
        callback(null,"Some data");
    }
  ],function(err,data) {
  // Code to execute after everything is done.
});

Example

function step1(callback) {
    setTimeout(function() {
        console.log('Done async step 1!');
        callback(null, 1, 2);
    }, 1000);
}

function step2(input1, input2, callback) {
    setTimeout(function() {
        console.log('Done async step 2!');
        callback(null, input1 + input2 + 3);
    }, 1000);
}

function step3(input3, callback) {
    setTimeout(function() {
        console.log('Done async step 3!');
        callback(null, input3 + 4);
    }, 1000);
}

// result is whatever is passed from step3 callback
function finalCallback(err, result) {
    // result == 10 (1 + 2 + 3 + 4)
    console.log('result: ' + result);
}

async.waterfall([
        function(callback) { 
            step1(callback);
        }, 
        function(input1, input2, callback) { 
            step2(input1, input2, callback);
        }, 
        function(input3, callback) { 
            step3(input3, callback);
        }, 
    ],
    finalCallback
);

Useful links