Avoiding callback hell in Node.js

March 15, 2019
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.

Update: Added the amazing async-await after it’s release in Node.js 7.10.0

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
  3. Using Async-Await

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 a 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 hell 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.

Also, another good part about chaining promises is that we can also add a catch block at the end of the chain to catch any error that occurs in any of the about functions.

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;
        }
    });
})
.catch(function(e){
     //handle error
     console.log(e);
});

 

Creating a function that returns a Promise

To create a function that works with promises we can use the in-built Promise class.

const test = function(){
    return new Promise((resolve, reject)=>{
        setTimeout(() => {
            resolve('done');
        }, 10);
    });
}

//call it like this
test().then((resp)=>{
    console.log(resp);
})
.catch((e)=>{
    console.log(e);
});

 

A promise is considered successful if the value is returned with the resolve method and unsuccessful if it is returned with the reject method.

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
});

 

Using Async Await

One of the best things to come out in Node.js recently is the async-await feature. Async await makes asynchronous code look like it’s synchronous. This has only been possible because of the reintroduction of promises into node.js. Async-Await only works with functions that return a promise.

Right now the best way to avoid callback hell is by using async-await in Node.js. Let’s understand it with an example.

async-await
const getrandomnumber = function(){
    return new Promise((resolve, reject)=>{
        setTimeout(() => {
            resolve(Math.floor(Math.random() * 20));
        }, 1000);
    });
}

const addRandomNumber = async function(){
    const sum = await getrandomnumber() + await getrandomnumber();
    console.log(sum);
}

addRandomNumber();

 

If you see above we have a function that returns us a random number after 1 sec. This function returns a promise. Using the await keyword we tell javascript to wait for the result before moving forward. But this await keyword only works within functions that are declared as async. When we declare a function as async, we are telling javascript to suspend the execution until the result arrives whenever it encounter the await keyword.

Now, in promises to handle error we could directly chain a catch block. How would you do it in the above case? Well, we can simply use a try-catch block.
So our example above will look like this…

async-await
const getrandomnumber = function(){
    return new Promise((resolve, reject)=>{
        setTimeout(() => {
            resolve(Math.floor(Math.random() * 20));
        }, 1000);
    });
}

const addRandomNumber = async function(){
    try {
        const sum = await getrandomnumber() + await getrandomnumber();
        console.log(sum);
    } catch (error) {
        //handle error
        console.log(error);
    }
}

addRandomNumber();

One more important thing to note is, whenever you declare a function as async that function will return a Promise

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 library and promises are the two de-facto solutions for dealing with callback hell. But that was only until the arrival of async-await. With async-await here you should immediately start using it in your programmes to clean up and organize your code. 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.

Keep learning…

About

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

Get notified on our new articles

Subscribe to get the latest on Node.js, Angular, Blockchain and more. We never spam!
First Name:
Email:

Leave a Comment