@jan_grz Blog

The lib that makes the callback hell less scary

September 04, 2016

Async lets you build sophisticated flows, it has great capabilities for retries and timeouts, and even wonders like async.queue or async.whilsts. Aync makes the code much more readable, and allows you to unleash the full power of Node.js - efficient asynchronous programming.

It lets you streamline error handling and passing.

In Node.js code you will see a lot of repetition, one of the most common repetitions is the if(err) block.

getA(params, (err, a)=>{
  if(err)return callback(err);

  return getB(params, (err, b)=>{
    if(err)return callback(err);
      //etc.
  })
});

Async usually breaks the flow if any error occurs - it allows you to get rid of the repetition, and handle errors in one place - the final callback.


My callback style: I name first level of async callbacks simply cb and the main callback, if I need to introduce more levels of callbacks - I name them accoringly to the async method they belong to (eachCallback will belong to async.each)

async.parallel(
  {
    a: (cb)=>getA(params, cb),
    b: (cb)=>getB(params, cb),
    c: (cb)=>getC(params, cb),
    d: (cb)=>getD(params, cb)
    //etc.
  },
  (err, results)=>{
      if(err) return console.error(err);

      console.log( (results.a + results.b) * (results.c + results.d) )
  }
);

Combining asynchronous and synchronous with async is a pleasure!

I find it more readable to write validation function like one below in synchronous manner.

function validateData(data) {
  if(!data.name) throw new Error('Name is missing');

  if(data.age < 0) throw new Error('Age cannot be negative');

  if(isNan(data.height)) throw new Error('Height must be a number');
}

I can safely plugin this function to an asynchronous flow using asyncify.

async.autoInject(
  {
    rawData: (cb)=>getData(params, cb),

    validate: (rawData, cb)=> async.asyncify(validateData)(rawData, cb),

    save: (rawData, validate, cb)=> db.save(rawData, cb)
  },
  callback
)

This way if any error is thrown inside validateData function it will be passed to the callback and break the autoInject flow. There is a hidden benefit to it - if rawData passed to validateData is undefined a TypeError: Cannot read property 'name' of undefined is throw

  • asyncify will also catch this error and pass it to the main callback

JSON.parse is a good subject for async.asyncify

If you are not using asyncify for your synchronous code remember to use setImmediate(cb), otherwise you may blow up the stack.

Async goes well with readability and functional programming

Assignments and temporary variables can lead to bugs. Async lets you write functions that begin with a return, without assignments, in more functional style. There is a lot of possible control flows with async - almost certainly you will find one that matches your needs, and allows to avoid assignments. It is easier to reason about code that uses async heavily.

In following example async is doing all of the pipelining.

function notifyFriends(id, message,  callback){
  return async.waterfall(
    [
      (cb)=>getUser(id, cb),

      (user, cb)=>getFriends(user.friends, cb),

      (friends, cb)=>{
        return async.eachSeries(friends,

          (friend, eachCallback)=>sendMessage(friend, message, eachCallback),

          cb
        );
      },

      (cb)=>saveMessage(message, cb)
    ],
    callback
  );
}

Aync is to Node what jelly is for peanutbutter


Jan Grzesik- a Team Lead, Full Stack Engineer, maker - living in Kraków, Poland

Copyright © 2022