Caolan Asyncjs vs. Async/Await: What to Use for Async Operations in Node.js?
Is async/await enough?

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, which 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 real-world projects today.
If not handled well, asynchronous operations can prove to be harmful in the 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 an easy-to-read way of working with asynchronous operations in nodeJS and if it’s still needed for the usual control flows.
Async operations in javascript
Asynchronous operations in nodeJS are the operations that cannot return the result immediately. It can be a network call or a database operation, for instance.
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:
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
).
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 an error in any function, no further handler will be executed and the control will directly jump to the final callback with the thrown error.
NOTE: The output array will contain the results in order of the functions written, regardless of their execution time.
async.parallel
This control flow comes in handy when the handlers are not dependent on each other at all. You can trigger all of them at once.
Parallel execution means starting off I/O tasks if any, if your functions do not perform any I/O, the functions will be run in series in a synchronous fashion.
Javascript will still process synchronous code blocks in series.
Again, an 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
, the result from the final callback will come from whichever function calls the callback first.
Using async/await
The control flows that we’ve seen in the previous section can be replicated using async/await without the need for the asyncjs library. Let’s recreate those examples using async/await:
async.series
Assuming the above code block is inside an async function, we have easily replicated the async.series
functionality here.
- We’re making sure that
asyncFnThatReturnsOne
resolves and returns the result first beforeasyncFnThatReturnsTwo
can run. - The final result array is exactly the same as before i.e., [‘One’, ‘Two’]. It does not matter whether
asyncFnThatReturnsOne
takes longer thanasyncFnThatReturnsTwo
. - We’re catching errors using try-catch block.
async.parallel
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:
However, asyncjs
library provides some benefits that make 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 a 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 up to a max limit known as concurrency limit. Tasks are picked up as soon as the concurrent workers running becomes less than the concurrency limit.
Feel free to play around by tweaking the concurrent_workers
number and see how it affects the async operations being processed. The playground link is 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 an unlimited number of them in parallel, your 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 toasync.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 successful response is received. For example, trying and failing the same request 3 times gives us certainty in our judgments of service behavior.
In the above example, we’re firing someAPIMethod
5 times with a 100ms interval. The callback is immediately called with the successful result
if any method succeeds. In case no method success, the callback is called with an error.
There are other control flows in asyncjs
that 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 save 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).
This article was originally published on rrawat.com.
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 going through the asyncjs
documentation and checking 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.
Want more such articles?You can join my newsletter here.I write about my learnings and experiences related to web development technologies biweekly.