Different ways of Async patterns in Node.js







callbacks, async module, promises, async/await, coroutines/generators, which to use and when.


1. Callbacks: simple, obvious, …hellish



const fs = require('fs');
fs.readFile('./hello.txt', 'utf-8', (err, data) => {
if (err) return console.error('Failed reading file:', err);
  console.log('File contents:', data);
});

Not much to be said about this. To the method doing something async, you pass a callback (taking the customary err, dataWhateverEtc arguments) which is to be called after the thing is done (or failed)… The first argument is null except when an error happens (generally you process the error path first in you code and return or throw if you have an error). Simple. Well fit for Javascript since functions are first class objects. Obvious. Why the hell would anyone want more?!

Well, ‘cause regular programs tend to need to do stuff in sequence, and when you have callbacks-async actions in sequence you end up with the “beloved” “pyramid”:

doSuff1(arg1, (err, ret1) => {
if (err) {
ifErrorDoStufE1(argE1, (errE1, retE1) => {
// ...
});
// ...
return stuffToReturnWhenErr1;
}
// ...
thenAfterDoStuff2(arg2, (err, ret2) => {
// ...
thenAfterDoStuff3(arg3, (err, ret3) => {
// ...
thenAfterDoStuff4(arg4, (err, ret4) => {
// ...
thenAfterDoStuff5(arg5, (err, ret5) => {
// ...
thenAfterDoStuff6(arg6, (err, ret6) => {
// ...
thenAfterDoStuff7(arg7, (err, ret7) => {
// ...
});
});
});
});
});
});
// ...
});

You can imagine that the real-life production version of this is way more complicated. And you can imagine debugging this and how the stack traces look like. This is why this resulting pattern is less affectionately called “callback hell”.

But wait… this is still the simplest case you can think of. Real-life code would need to do things like “when both async actions A and B are done (hopefully in parallel if they do IO) do C” or “when the first of async actions A, B and C finishes, do D”. Let’s check out how the first case looks with callbacks-async code:


let leftToDo = 2; // or use booleans doneA and doneB instead...
doA(argA, (errA, resA) => {
// ...
leftToDo--;
// ...to do after A
if (leftToDo === 0) {
todoAfterAB();
}
});
doA(argA, (errA, resA) => {
// ...
leftToDo--;
// ...to do after A
if (leftToDo === 0) {
todoAfterAB();
}
});
function todoAfterAB() {
// ...
}

This seems almost OK… until you realize that this is the simplest imaginable case and that those // ...s can be tens/hundreds of LOC. Enter:


2. Async module: the callbacks wrangling cowboy to the rescue!


The well known and loved 3rd party module async comes to the rescue with a well thought of set of async helpers that allows one to organize async callbacks in sane ways even in complex scenarios. It’s basically what you yourself would’ve invented after working with code like the one above for long enough… but the problem is that each developer would come up with his/her own slightly different way of handling these patterns! Standardization in handling callbacks-async code is the true feature of the async module. If you’re not familiar with it, you should, because you’ll definitely come across code using it, and you’ll have to be able to understand it, even if imho you should jump over it in your own code: if callbacks get too messy, just jump to Promises! …that’s my 2 cents of advice ;)


Here’s how a slightly more complicated variant of the code above looks like using async :

const async = require('async');
async.parallel(
[
doA, // if A needs no args and no special logic afterwards
doB.bind(null, argB), // if B needs argB
(callback) => { // if C needs both arg and special logic
// ...stuff to do immediately before C
doC(argC, (errC, resC) => {
// ...stuff to do immediately after C
callback(resC);
});
}
],
(err, results) => { // callback
// ...stuff to do after A, B and C are all done
}
);

This uses 3 async callbacks, it also takes care of collecting the results of all of them (in the results array), and it also shows how to handle async functions what need params and special logic happening immediately before and after an async callback.


The name, “parallel” is a bit confusing, because as you know Node.js is single threaded, so the only thing happening in parallel is the IO, done outside Javascript code (so don’t ever think of using async.parallel for parallelizing CPU intensive code in Node.js, despite its name).


The other nice consequence is that code doesn’t “float to the right” that much, even though you still need some nesting.


So, uhm… that’s it, right? “Callback hell” / “pyramid code” was the problem, and the async module is the solution, no?