Caolan Asyncjs vs Async/Await: Which One to Use 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 }17})
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).
1async.series([2 function(callback) {3 setTimeout(function() {4 // do some async task5 callback(null, 'one');6 }, 200);7 },8 function(callback) {9 setTimeout(function() {10 // then do another async task11 callback(null, 'two');12 }, 100);13 }14], function(err, results) {15 console.log(results);16 // results is equal to ['one','two']17});
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.
1async.parallel([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 though15 // the second function had a shorter timeout.16});
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.
1async.race([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],13// main callback14function(err, result) {15 // the result will be equal to 'two' as it finishes earlier16});
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
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);7}
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. - Final result array is exactly same as before i.e., ['One', 'Two']. It does not matter whether
asyncFnThatReturnsOne
takes longer thanasyncFnThatReturnsTwo
. - We're catching error using try-catch block.
async.parallel
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);8}
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:
async.race example code using promises
1const promise1 = new Promise((resolve, reject) => {2 setTimeout(resolve, 500, 'one');3});45const promise2 = new Promise((resolve, reject) => {6 setTimeout(resolve, 100, 'two');7});89// Both resolve, but promise2 is faster10const 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.
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.
1const async = require('async');23// specify how many worker execute task concurrently in the queue4const concurrent_workers = 1;56const queue = async.queue((object, callback) => {7 let date = new Date();8 let time = date.toISOString();910 // Log processing start time11 console.log(`Start processing movie ${object.movie} at ${time}`);1213 // simulated async operation, can be network/DB interaction14 setTimeout(() => {15 date = new Date();16 time = date.toISOString();1718 // Log processing end time19 console.log(`End processing movie ${object.movie} at ${time} \n`);20 callback(null, object.movie);21 }, 1000);22}, concurrent_workers);2324queue.drain(function () {25 console.log('all items have been processed');26});2728// add total of 8 tasks to be processed by the worker function29for (let i = 0; i < 8; i++) {30 queue.push({ movie: `Spiderman ${i}`, excitement: `${100 * i}` });31 console.log(`queue length: ${queue.length()}`);32}
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.
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.
1async.retry(2 {times: 5, interval: 100},3 someAPIMethod,4 function(err, result) {5 // process the result6});
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.