How to Elegantly Solve the Callback Hell of NodeJS and Express with Async.js

Initially writing NodeJS and Express looks fun. It is JavaScript, it’s simple and it is super fast.

However, the more your application complexity increases, you begin to notice the endless callbacks.

You have one, inside there is another, inside it is one more and sometimes, even 5 or 6 more.

Everything of course is indented, which makes it actually harder to read.

This is also known as Callback Hell.

There are some solutions on the Internet. For example, you can try to define every function not as an anonymous callback but separate named function.

It helps, but you lose the benefits from closures and the functions are only used at one place.

Another solution is to use promises. However, by default NodeJS is made for callbacks and sometimes using promises can be a little awkward to use.

In addition, some implementation of promises are slow, much slower than using callbacks.

Another solution is to use ES6 generators, but not all frameworks support them. Moreover, they feel alien compared to the rest of JavaScript. In addition, you might need Babel or another library to convert ES6 to regular JavaScript because Node doesn’t support all ES6 features, yet.

Wouldn’t it be nice if?

  • You can still use callbacks
  • You can have as many callbacks as you want
  • Your app is super quick
  • Everything is very east to read and maintain

Async.js

There is a solution in the form of the library Async.js.

$ npm install async

Then you can use it by including in your JavaScript files

var async = require('async')

Async provides many useful methods which can solve all kind of situations while keeping your code maintainable and easy to read.

I use it in each and everyone of my NodeJS and Express projects. Actually, it even works in the browser, when you need it.

Let’s have a look at a few common situations and how async will help with them.

Getting data from multiple independent sources

For example, you might have a dashboard where you display multiple independent stats about your app. One displays your app memory usage over time, another cpu usage and one last the user retention.

router.get('/dashboard', function(req, res) {
  Stats.getMemoryUsage(function(err, memory) {
    Stats.getCPUUsage(function(err, cpu) {
      Stats.getUserRetention(function(err, retention) {
        res.render('dashboard', {memory: memory, cpu: cpu, retention: retention})
      })
    })
  })
})

Not great, isn’t it? Let’s have a look how async.series can help you

router.get('/dashboard', function(req, res) {
  async.series([
    function(callback) {
      Stats.getMemoryUsage(callback)
    },
    function(callback) {
      Stats.getCPUUsage(callback)
    },
    function(callback) {
      Stats.getUserRetention(callback)
    }
  ],
  function(err, results) {
    res.render('dashboard', {memory: results[0], cpu: results[1], retention: results[2]})
  })
})

Isn’t this better? No more endless indenting.

In fact, in this case, we can make it even better because all three methods don’t depend on the Stats object.

router.get('/dashboard', function(req, res) {
  async.series([
    Stats.getMemoryUsage,
    Stats.getCpuUsage,
    Stats.getUserRetention
  ],
  function(err, results) {
    res.render('dashboard', {memory: results[0], cpu: results[1], retention: results[2]})
  })
})

This is the true power of async to make your code shorter and easier to read.

Getting data from multiple dependent source

We saw what you can do when the data sources are independent, but more often than not they actually depend on each other.

Thankfully async has you covered in this case, too.

Let’s have a look at a simplified social network feature where you are shown stories of the friends of your friends, with the hope that you might like them and become friends with them, too.

router.get('/dashboard', function(req, res) {
  Friends.all(req.sesssion.user, function(err, friends) {
    Friends.all(friends, function(err, friends_of_friends) {
      users = friends_of_friends.filter(function(friend) {
        return !friend.isBlocked(req.sesssion.user)
      })

      Stories.all(users, function(err, stories) {
        res.render('dashboard', {stories: stories})
      })
    })
  })
})

Your friends are read from the database, then their friends are retrieved and you filter those who might have blocked you for some reason, or you might have blocked them. Finally, the stories of their friends.

Fortunately there is async.waterfall to save you. It looks almost as async.series but in addition to the callback, each function also provides the result from the previous callback.

router.get('/dashboard', function(req, res) {
  async.waterfall([
    function(callback) {
      Friends.all(req.sesssion.user, callback)
    },
    function(friends, callback) {
      Friends.all(friends, callback)
    },
    function(friends_of_friends, callback) {
      users = friends_of_friends.filter(function(friend) {
        return !friend.isBlocked(req.sesssion.user)
      })

      Stories.all(users, callback)
    }
  ],
  function(err, stories) {
    res.render('dashboard', {stories: stories})
  })
})

It is already clear that async removes the callback hell, but what’s more it makes your code much easier to reason about. It provides a flow which is easy to read and follow.

Just like before, this can be simplified a little bit more.

router.get('/dashboard', function(req, res) {
  async.waterfall([
    function(callback) {
      Friends.all(req.sesssion.user, callback)
    },
    Friends.all,
    function(friends_of_friends, callback) {
      users = friends_of_friends.filter(function(friend) {
        return !friend.isBlocked(req.sesssion.user)
      })

      Stories.all(users, callback)
    }
  ],
  function(err, stories) {
    res.render('dashboard', {stories: stories})
  })
})

Processing asynchronously a large list of items

Most of the time async.waterfall and async.series will help you with your callbacks, but you will quickly find that when you have to deal with large number of calls to the same asynchronous function, they are not so useful anymore.

Thankfully there are functions like async.map which can help you.

Let’s have a look at another example where you have to read data from multiple blogs, but you only have their urls.

This time I will immediately show you the async version.

function process(urls, done) {
  async.map(
    urls,
    function(url, callback) {
      read_data_from_blog(url, callback)
    },
    function(err, results) {
      done(err, results)
    }
  )
}

This again can be simplified

function process(urls, done) {
  async.map(urls, read_data_from_blog, done)
}

It could not get more simple than that.

Next

You have seen how async.js can make your life much easier and your code better.

Your next step should be to look at your code and use async where it can help you. You will see how much more readable and maintainable it becomes.

In addition, there are many more helpful methods provided by async.js, than what I just show you. Check its documentation to see what other helpful functions you can find.

Also in all examples above we expect everything to go well, but async can also handle perfectly situations where errors occur.


Other articles that you may like

Did you like this article?

Please share it

We are Stefan Fidanov & Vasil Lyutskanov. We share actionable advice about development with Node, Express, React and other web & mobile technologies.

It is everything that we have learned from years of experience working with customers from all over the world on projects of all sizes.

Let's work together
© 2024 Terlici Ltd · Terms · Privacy