DEV Community

Cover image for Caolan Asyncjs vs Async/Await: Which One to Use for Async Operations in NodeJS
Rishabh Rawat
Rishabh Rawat

Posted on • Originally published at rrawat.com

Caolan Asyncjs vs Async/Await: Which One to Use for Async Operations in NodeJS

Working with JavaScript we all have come across asynchronous operations at some point in our web development journey. There are various ways you can handle an asynchronous operation in JavaScript/nodeJS, can be either using callbacks, promises or async/await. This gives developers so much flexibility in code and that's the reason you can still find different approaches in the real world projects today.

If not handled well, asynchronous operations can prove to be harmful in subtlest of ways. We all know callback hell right?

In this article we'll take a look at Caolan's asyncjs library, how it provides easy-to-read way of working with asynchronous operations in JavaScript/nodeJS and if it's still needed for the usual control flows.

Here's the overview of what we'll cover:

  • ✨ Async operations in javascript
  • πŸ‘“ Handling async flows with asyncjs
  • πŸ§ͺ Using async/await
  • πŸ”Ž You might still need asyncjs
  • 🧩 Conclusion
  • πŸ„πŸΌβ€β™‚οΈ What next?

Let's jump right in 🏊

Async operations in javascript

Asynchronous operations in nodeJS/JS are the operations that cannot return the result immediately. It can be a network call or a database operation, for example.

As it does not make sense for the execution to get halted there waiting for the async operation to finish, callbacks & promises came to solve the problem.

With callback/promise, we tell the event loop what to do when the result of the async operation arrives.

The callback/promise gets pushed to the event loop and gets revisited in the next iteration. This process repeats if the async operation doesn't resolve by the next iteration of the event loop.

Here's a sample callback based approach of working with async operations:

someAsyncOperation(function (err, data) {
  if (err) {
    console.log(`Some error occurred. Look at it => ${err}`);
  } else {
    data.forEach((item, index) {
      asyncProcessingOfItem(item, function (itemErr, isProcessed) {
        if (itemErr) {
          console.log(`Some error occurred while processing item. Here's that beast => ${err}`);
        } else if (isProcessed) {
          console.log(`${item} processed succesfully!!!`);
        } else {
          console.log(`${item} could not be processed :(`); 
        }
      })
    })
  }
})
Enter fullscreen mode Exit fullscreen mode

Yes, the code doesn't look clean and credit goes to callbacks. If you want to understand more about callbacks and callback hell, there's a whole website dedicated to this. Check it out here.

This situation was vastly improved with the asyncjs library. Let’s see how asyncjs library contributed to better readability πŸ‘‡

Handling async flows with asyncjs

The library provides an easy way to deal with asynchronous functions in NodeJS. In addition to a good collection of functions for arrays and objects, there are various control flows that the library provides for making developers life easy.

Asyncjs library also provides support for promises and async/await but I'll be showing examples using callbacks.

async.series

This flow allows you to put as many handlers as you want and they'll run in series one after the other. The output of one does not depend on the previous handler (unlike async.waterfall).

async.series([
    function(callback) {
        setTimeout(function() {
            // do some async task
            callback(null, 'one');
        }, 200);
    },
    function(callback) {
        setTimeout(function() {
            // then do another async task
            callback(null, 'two');
        }, 100);
    }
], function(err, results) {
    console.log(results);
    // results is equal to ['one','two']
});
Enter fullscreen mode Exit fullscreen mode

In the above example, two async functions run in series and the final callback contains an array with the returned values from those functions.

If there's any error in any function, no further handler will be executed and the control will directly jump to the final callback with the thrown error.

async.parallel

This control flow comes handy when the handlers are not dependent on each other at all. You can trigger all of them at once. By parallel, we only mean kicking off I/O tasks if any, if your functions do not perform any I/O or use any timers, the functions will be run in series in synchronous fashion. Javascript is still single-threaded.

async.parallel([
    function(callback) {
        setTimeout(function() {
            callback(null, 'one');
        }, 200);
    },
    function(callback) {
        setTimeout(function() {
            callback(null, 'two');
        }, 100);
    }
], function(err, results) {
    console.log(results);
    // results is equal to ['one','two'] even though
    // the second function had a shorter timeout.
});
Enter fullscreen mode Exit fullscreen mode

Again, error in any of the handlers will cause the execution of all the remaining handlers to be skipped.

async.race

This is exactly similar to Promise.race, result from the final callback will come from whichever function calls the callback first.

async.race([
    function(callback) {
        setTimeout(function() {
            callback(null, 'one');
        }, 200);
    },
    function(callback) {
        setTimeout(function() {
            callback(null, 'two');
        }, 100);
    }
],
// main callback
function(err, result) {
    // the result will be equal to 'two' as it finishes earlier
});
Enter fullscreen mode Exit fullscreen mode

Using async/await

The control flows that we've seen in the previous section can be replicated using async/await without the need of asyncjs library. Let's recreate those examples using async/await:

async.series

try {
  const resultFromFn1 = await asyncFnThatReturnsOne();
  const resultFromFn2 = await asyncFnThatReturnsTwo();
  return [resultFromFn1, resultFromFn2];
} catch (err) {
  console.log(err);
}
Enter fullscreen mode Exit fullscreen mode

Assuming the above code block is inside an async function, we have easily replicated the async.series functionality here.

  1. We're making sure that asyncFnThatReturnsOne resolves and returns the result first before asyncFnThatReturnsTwo can run.
  2. Final result array is exactly same as before i.e., ['One', 'Two']. It does not matter whether asyncFnThatReturnsOne takes longer than asyncFnThatReturnsTwo.
  3. We're catching error using try-catch block.

async.parallel

try {
  const result = await Promise.all([    // result = ['One', 'Two']
    asyncFnThatReturnsOne(),
    asyncFnThatReturnsTwo()
  ]);
} catch (err) {
  console.log(err);
}
Enter fullscreen mode Exit fullscreen mode

We're firing both async functions in parallel and have wrapped them in Promise.all. We're awaiting that and voila, we have the same result!

async.race

Similarly, we can use promises to recreate a race scenario without needing asyncjs library:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

// Both resolve, but promise2 is faster
const result = await Promise.race([promise1, promise2]);
console.log(result);  // output = 'two'
Enter fullscreen mode Exit fullscreen mode

However, asyncjs library provides some benefits that makes it worth it. One thing to keep in mind, it is possible to make your own custom solution and recreate everything from scratch. But it is generally not good idea to reinvent the wheel when there's already a library that does exactly what you want.

You might still need asyncjs

We have seen a few scenarios where it doesn't make much sense to install asyncjs library. But there are other use-cases where asyncjs can prove worthy and save you from writing your own custom solutions.

async.queue

This queue utility helps you write a worker function and provide a set of tasks to be processed by the worker function. Tasks are run in parallel upto a max limit known as concurrency limit. Tasks are picked up as soon as the concurrent workers running becomes less than the concurrency limit.

const async = require('async');

// specify how many worker execute task concurrently in the queue
const concurrent_workers = 1;

const queue = async.queue((object, callback) => {
  let date = new Date();
  let time = date.toISOString();

  // Log processing start time
  console.log(`Start processing movie ${object.movie} at ${time}`);

  // simulated async operation, can be network/DB interaction
  setTimeout(() => {
    date = new Date();
    time = date.toISOString();

    // Log processing end time
    console.log(`End processing movie ${object.movie} at ${time} \n`);
    callback(null, object.movie);
  }, 1000);
}, concurrent_workers);

queue.drain(function () {
  console.log('all items have been processed');
});

// add total of 8 tasks to be processed by the worker function
for (let i = 0; i < 8; i++) {
  queue.push({ movie: `Spiderman ${i}`, excitement: `${100 * i}` });
  console.log(`queue length: ${queue.length()}`);
}
Enter fullscreen mode Exit fullscreen mode

Feel free to play around by tweaking the concurrent_workers number and see how it affects the async operations being processed. Playground link available here.

This is very useful in making sure that you don't attempt to run more tasks in parallel than your CPU/disk can take. Remember, the parallel aspect is only for the I/O and timers. If all of your tasks have I/O and you're running unlimited number of them in parallel, you're server will crash because of high Disk I/O usage and resource starvation.

async.queue provides a good use-case of throttling applications because of the ability to set a max cap on the number of parallel execution.

Check out async.priorityQueue which is similar to async.queue but offers the priority mechanism to make sure higher priority tasks don't starve out because of low priority tasks.

async.retry

It is sometimes possible that a request fails with no fault of our application (eg. network connection issue). You can use async.retry to make the same request X number of times until a success response is received. For example, trying and failing the same request 3 times gives us certainty in our judgments of service behavior.

async.retry(
  {times: 5, interval: 100},
  someAPIMethod,
  function(err, result) {
    // process the result
});
Enter fullscreen mode Exit fullscreen mode

In above example, we're firing someAPIMethod 5 times with a 100ms interval. Callback is immediately called with the successful result if any method succeeds. In case no method success, callback is called with an error.

There are other control flows in asyncjs which can come in really handy, you can check them out here.

Conclusion

This was a short overview of asyncjs library, some of the control flows it provides and how we can replicate the same flows using async/await. We also looked at a few cases where using asyncjs can prove really helpful and saves you from reinventing the wheel.

I hope it gave you some perspective on the benefits of the library and how we should understand our specific use-case before jumping onto 3rd party solutions (one commit is enough sometimes πŸ™ƒ)

What next?

The documentation of asyncjs is quite straightforward and easy to read. As we've only seen a couple of use cases in this article, I'd recommend to go the asyncjs documentation and check out other possibilities with the library. You can also try to replicate the same using async/await to solidify your understanding of where the library might still make sense.

Top comments (0)