Avoiding callback hell in Node.js

June 12, 2016
by Rahil Shaikh

Callback hell is a phenomenon that afflicts a JavaScript developer when he tries to execute multiple asynchronous operations one after the other. Some people call it to be the pyramid of doom.
Let’s have a look at an example of what we call callback hell.

doSomething(param1, param2, function(err, paramx){
    doMore(paramx, function(err, result){
        insertRow(result, function(err){
            yetAnotherOperation(someparameter, function(s){
                somethingElse(function(x){
                });
            });
        });
    });
});

Note- The above code is for demonstration and not a working code.

This is looking bad just from the skeleton itself. In real code you will obviously have other statements like if, for or some other operations along with these nested functions. Add those to the above code and it would get really messy and unmanageable.

Pro Tip

Do not overload your functions. Most of the times it might be possible that the real problem you are facing is not the callback hell, rather it’s poorly written functions. In such cases, the solutions we will see below would structure your code but they won’t solve your real problem. So, as a rule of thumb, make your function do less, meaning write small functions that accomplish a single task.

Techniques for avoiding callback hell

There are multiple techniques for dealing with callback hell. In this tutorial, we will have a look at the below two in particular.

  1. Using Async.js
  2. Using Promises

Managing callbacks using Async.js

Async is a really powerful npm module for managing asynchronous nature of JavaScript. Along with Node.js, it also works for JavaScript written for browsers.

Async provides lots of powerful utilities to work with asynchronous processes under different scenarios.

Installation
npm install --save async

 

async.waterfall()

For our problem, we will be looking at two functions of async in particular. i.e async.waterfall() and async.series().
Async Waterfall is useful when you want to execute some tasks one after the other and at the same time pass on the result from the previous task to the next.

async.waterfall() takes in an array of functions ‘tasks‘ and a final ‘callback‘ function which is called after all the functions in tasks have completed or a callback is called with an error.
Have a look below for better understanding.

Async Waterfall
var async = require('async');
async.waterfall([
    function(callback) {
        //doSomething
        callback(null, paramx); //paramx will be availaible as the first parameter to the next function
        /**
            The 1st parameter passed in callback.
            @null or @undefined or @false control moves to the next function
            in the array
            if @true or @string the control is immedeatly moved
            to the final callback fucntion
            rest of the functions in the array
            would not be executed
        */

    },
    function(arg1, callback) {
        //doSomething else
      // arg1 now equals paramx
        callback(null, result);
    },
    function(arg1, callback) {
        //do More
        // arg1 now equals 'result'
        callback(null, 'done');
    },
    function(arg1, callback) {
        //even more
        // arg1 now equals 'done'
        callback(null, 'done');
    }
], function (err, result) {
    //final callback function
    //finally do something when all function are done.
    // result now equals 'done'
});

So using async.waterfall(), you would be writing you code vertically instead of indenting it horizontally and entering the pyramid of doom. Plus your code would be much more organized and easy to read.

async.series()

Async provides another function for handling execution in series, async.series(). Async series works in a similar way to Async waterfall, by executing functions in the array one after the other with the difference that it won’t pass the data from one function to another, instead when all the functions have completed their execution the result of the functions will be available in the final callback as an array. Similar to async.waterfall() in async.series() as well, when any of the functions is called with an error callback, no more functions in the array are executed and the final callback is immediately called with the value of the error. Have a look below for a clear picture.

Async Series
var async = require('async');
async.series([
    function(callback){
        // do some stuff ...
        callback(null, 'one');
        /**
            The 1st parameter passed in callback.
            @null or @undefined or @false control moves to the next function
            in the array
            if @true or @string the control is immedeatly moved
            to the final callback fucntion with the value of err same as
            passed over here and
            rest of the functions in the array
            would not be executed
        */

    },
    function(callback){
        // do some more stuff ...
        callback(null, 'two');
    }
],
// optional callback
function(err, results){
    // results is now equal to ['one', 'two']
});

 
There are more cool functions availaible with the async module make sure you check them out and use it in your Node.js projects.
Visit the Github page here: Async.js Github

Managing callbacks using promises

Promises are alternative to callbacks, while dealing with asynchronous code. Promises return the value of the result or an error exception. The core of the promises is the .then() function, which waits for the promise object to be returned. The .then() function takes two optional functions as arguments and depending on the state of the promise only one will ever be called. The first function is called when the promise if fulfilled (A successful result). The second function is called when the promise is rejected.
Let us see the structure of a typical promise.

Promises
var outputPromise = getInputPromise().then(function (input) {
    //handle success
}, function (error) {
    //handle error
});

 

Chaining promises

We can also chain promises, this is an equivalent alternative to nesting callbacks. There are two ways to chaining promises. Promises can be chained inside or outside the handlers (.then() function). Chaining promises outside the handle is much clean and easy to read but we may have to chain promises inside the handler if we want to use some parameter in our next handler which available in the scope of the previous handler. Although too much chaining inside the handler will again result in horizontal code which we are trying to avoid. We can also chain promises with a combination of inside and outside chains. As a general rule, I would recommend you to avoid chaining inside the handler.

Chaining Promises
return getUsername().then(function (username) {
    return getUser(username);
})
.then(function (user) { //example of chaining outside the handler
   return userPassword().then(function(password){
        /**
            example of chaining inside the handler,
            since we need to use the @user param
            from the previous handler scope
        */

        if(user.password !== password){
            //reject promise or throw error
            return;
        }
    });
});

 

Changing callbacks to promises

There are a few good libraries for promises, we will look at the examples from the popular Kriskowal’s q promises library. Make sure you have q installed.

npm install --save q

callbacks to promises
var fs = require('fs');
var Q = require('q');
var readFile = Q.nfbind(fs.readFile);
readFile("foo.txt", "utf-8").then(function (text) {
   //handle success
}, function(err){
   //handle error
});

 

Writing functions that work with promises

We can also write our own function that works with promises. Look at the example below.

functions that work with promises
var fs = require('fs');
var Q = require('q');
//creation
function read(file) {
    var deferred = Q.defer();
    fs.readFile(file, 'UTF-8', function(err, data){
        if(err) {
            deferred.reject(err); //faliure, control will return to the second function
        } else {
            deferred.resolve(data) // fulfills the promise and returns data as value
        }
    });
        return deferred.promise // our mthod returns promise
}
//usage
read('demo.txt').then(function(data){
    //promise resolved with data
}, function(err){
    //promise rejected with err
});

 
There is more to promises, headover to the Github page to find out more.

Conclusion

Thus, we have seen, how we can deal with the problem of callback hell in Node.js. There are a few more ways to solve the problem like using generators, modularization etc. But we feel that async and promises are the two de-facto solutions for dealing with callback hell, with async preferred more over promises. However, remember what we discussed earlier, your core problem might just not be the callback hell, it could be poorly written functions. Make sure you keep your functions skinny and focused on a single task. You will learn more about async and promises only when you will use them. So make sure you try them out next time in your projects.

About

Engineer. Blogger. Thinker. Loves programming and working with emerging tech. We can also talk on Football, Gaming, World Politics, Monetary Systems.

Free PDF

Subscribe and get AngularJS Tips PDF. We never spam!
First Name:
Email:

Leave a Comment