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

Last updated on 22 February 2022


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:

callback example for handling async operations

1someAsyncOperation(function (err, data) {
2 if (err) {
3 console.log(`Some error occurred. Look at it => ${err}`);
4 } else {
5 data.forEach((item, index) {
6 asyncProcessingOfItem(item, function (itemErr, isProcessed) {
7 if (itemErr) {
8 console.log(`Some error occurred while processing item. Here's that beast => ${err}`);
9 } else if (isProcessed) {
10 console.log(`${item} processed succesfully!!!`);
11 } else {
12 console.log(`${item} could not be processed :(`);
13 }
14 })
15 })
16 }

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.


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).

2 function(callback) {
3 setTimeout(function() {
4 // do some async task
5 callback(null, 'one');
6 }, 200);
7 },
8 function(callback) {
9 setTimeout(function() {
10 // then do another async task
11 callback(null, 'two');
12 }, 100);
13 }
14], function(err, results) {
15 console.log(results);
16 // results is equal to ['one','two']

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.


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.

2 function(callback) {
3 setTimeout(function() {
4 callback(null, 'one');
5 }, 200);
6 },
7 function(callback) {
8 setTimeout(function() {
9 callback(null, 'two');
10 }, 100);
11 }
12], function(err, results) {
13 console.log(results);
14 // results is equal to ['one','two'] even though
15 // the second function had a shorter timeout.

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


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

2 function(callback) {
3 setTimeout(function() {
4 callback(null, 'one');
5 }, 200);
6 },
7 function(callback) {
8 setTimeout(function() {
9 callback(null, 'two');
10 }, 100);
11 }
13// main callback
14function(err, result) {
15 // the result will be equal to 'two' as it finishes earlier

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:


serial execution example using async/await

1try {
2 const resultFromFn1 = await asyncFnThatReturnsOne();
3 const resultFromFn2 = await asyncFnThatReturnsTwo();
4 return [resultFromFn1, resultFromFn2];
5} catch (err) {
6 console.log(err);

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.


parallel execution example using async/await

1try {
2 const result = await Promise.all([ // result = ['One', 'Two']
3 asyncFnThatReturnsOne(),
4 asyncFnThatReturnsTwo()
5 ]);
6} catch (err) {
7 console.log(err);

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!


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

async.race example code using promises

1const promise1 = new Promise((resolve, reject) => {
2 setTimeout(resolve, 500, 'one');
5const promise2 = new Promise((resolve, reject) => {
6 setTimeout(resolve, 100, 'two');
9// Both resolve, but promise2 is faster
10const result = await Promise.race([promise1, promise2]);
11console.log(result); // output = 'two'

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.


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.

Run Code

1const async = require('async');
3// specify how many worker execute task concurrently in the queue
4const concurrent_workers = 1;
6const queue = async.queue((object, callback) => {
7 let date = new Date();
8 let time = date.toISOString();
10 // Log processing start time
11 console.log(`Start processing movie ${} at ${time}`);
13 // simulated async operation, can be network/DB interaction
14 setTimeout(() => {
15 date = new Date();
16 time = date.toISOString();
18 // Log processing end time
19 console.log(`End processing movie ${} at ${time} \n`);
20 callback(null,;
21 }, 1000);
22}, concurrent_workers);
24queue.drain(function () {
25 console.log('all items have been processed');
28// add total of 8 tasks to be processed by the worker function
29for (let i = 0; i < 8; i++) {
30 queue.push({ movie: `Spiderman ${i}`, excitement: `${100 * i}` });
31 console.log(`queue length: ${queue.length()}`);

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.


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.

2 {times: 5, interval: 100},
3 someAPIMethod,
4 function(err, result) {
5 // process the result

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.


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.

No spam. Unsubscribe at any time.