Promises in Javascript has been around for a long time now. It helped solve the problem of callback hell. But as soon as the requirements get complicated with control flows, promises start getting unmanageable and harder to work with. This is where async flows come to the rescue. In this blog, letโs talk about the various async flows which are used frequently rather than raw promises and callbacks.
Async Utility Module
Async is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript. Although it is built on top of promises, it makes asynchronous code look and behave a little more like synchronous code, making it easier to read and maintain.
Async utility has a number of control flows. Letโs discuss the most popular ones and their use cases:
1. Parallel
When we have to run multiple tasks independent of each other without waiting until the previous task has completed, parallel comes into the picture.
async.parallel(tasks, callback)
Tasks: A collection of functions to run. It can be an array, an object or any iterable.
Callback: This is the callback where all the task results are passed and is executed once all the task execution has completed.
In case an error is passed to a function’s callback, the main callback is immediately called with the error. Although parallel is about starting I/O tasks in parallel, itโs not about parallel execution since Javascript is single-threaded.
An example of Parallel is shared below:
async.parallel([
function(callback) {
setTimeout(function() {
console.log('Task One');
callback(null, 1);
}, 200);
},
function(callback) {
setTimeout(function() {
console.log('Task Two');
callback(null, 2);
}, 100);
}
],
function(err, results) {
console.log(results);
// the results array will equal [1, 2] even though
// the second function had a shorter timeout.
});
// an example using an object instead of an array
async.parallel({
task1: function(callback) {
setTimeout(function() {
console.log('Task One');
callback(null, 1);
}, 200);
},
task2: function(callback) {
setTimeout(function() {
console.log('Task Two');
callback(null, 2);
}, 100);
}
}, function(err, results) {
console.log(results);
// results now equals to: { task1: 1, task2: 2 }
});
2. Series
When we have to run multiple tasks which depend on the output of the previous task, series comes to our rescue.
async.series(tasks, callback)
Tasks: A collection of functions to run. It can be an array, an object or any iterable.
Callback: This is the callback where all the task results are passed and is executed once all the task execution has completed.
Callback function receives an array of result objects when all the tasks have been completed. If an error is encountered in any of the task, no more functions are run but the final callback is called with the error value.
An example of Series is shared below:
async.series([
function(callback) {
console.log('one');
callback(null, 1);
},
function(callback) {
console.log('two');
callback(null, 2);
},
function(callback) {
console.log('three');
callback(null, 3);
}
],
function(err, results) {
console.log(result);
// results is now equal to [1, 2, 3]
});
async.series({
1: function(callback) {
setTimeout(function() {
console.log('Task 1');
callback(null, 'one');
}, 200);
},
2: function(callback) {
setTimeout(function() {
console.log('Task 2');
callback(null, 'two');
}, 300);
},
3: function(callback) {
setTimeout(function() {
console.log('Task 3');
callback(null, 'three');
}, 100);
}
},
function(err, results) {
console.log(results);
// results is now equal to: { 1: 'one', 2: 'two', 3:'three' }
});
3. Waterfall
When we have to run multiple tasks which depend on the output of previous task, Waterfall can be helpful.
async.waterfall(tasks, callback)
Tasks: A collection of functions to run. It can be an array, an object or any iterable structure.
Callback: This is the callback where all the task results are passed and is executed once all the task execution has completed.
It will run one function at a time and pass the result of the previous function to the next one.
An example of Waterfall is shared below:
async.waterfall([
function(callback) {
callback(null, 'Task 1', 'Task 2');
},
function(arg1, arg2, callback) {
// arg1 now equals 'Task 1' and arg2 now equals 'Task 2'
let arg3 = arg1 + ' and ' + arg2;
callback(null, arg3);
},
function(arg1, callback) {
// arg1 now equals 'Task1 and Task2'
arg1 += ' completed';
callback(null, arg1);
}
], function(err, result) {
// result now equals to 'Task1 and Task2 completed'
console.log(result);
});
// Or, with named functions:
async.waterfall([
myFirstFunction,
mySecondFunction,
myLastFunction,
], function(err, result) {
// result now equals 'Task1 and Task2 completed'
console.log(result);
});
function myFirstFunction(callback) {
callback(null, 'Task 1', 'Task 2');
}
function mySecondFunction(arg1, arg2, callback) {
// arg1 now equals 'Task 1' and arg2 now equals 'Task 2'
let arg3 = arg1 + ' and ' + arg2;
callback(null, arg3);
}
function myLastFunction(arg1, callback) {
// arg1 now equals 'Task1 and Task2'
arg1 += ' completed';
callback(null, arg1);
}
4. Queue
When we need to run a set of tasks asynchronously, queue can be used. A queue object based on an asynchronous function can be created which is passed as worker.
async.queue(task, concurrency)
Task: Here, it takes two parameters, first – the task to be performed and second – the callback function.
Concurrency: It is the number of functions to be run in parallel.
async.queue returns a queue object that supports few properties:
- push: Adds tasks to the queue to be processed.
- drain: The drain function is called after the last task of the queue.
- unshift: Adds tasks in front of the queue.
An example of Queue is shared below:
// create a queue object with concurrency 2
var q = async.queue(function(task, callback) {
console.log('Hello ' + task.name);
callback();
}, 2);
// assign a callback
q.drain = function() {
console.log('All items have been processed');
};
// add some items to the queue
q.push({name: 'foo'}, function(err) {
console.log('Finished processing foo');
});
q.push({name: 'bar'}, function (err) {
console.log('Finished processing bar');
});
// add some items to the queue (batch-wise)
q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function(err) {
console.log('Finished processing item');
});
// add some items to the front of the queue
q.unshift({name: 'bar'}, function (err) {
console.log('Finished processing bar');
});
5. Priority Queue
It is the same as queue, the only difference being that a priority can be assigned to the tasks which is considered in ascending order.
async.priorityQueue(task,concurrency)
Task: Here, it takes three parameters:
- First – task to be performed.
- Second – priority, it is a number that determines the sequence of execution. For array of tasks, the priority remains same for all of them.
- Third – Callback function.
The async.priorityQueue does not support โunshiftโ property of the queue.
An example of Priority Queue is shared below:
// create a queue object with concurrency 1
var q = async.priorityQueue(function(task, callback) {
console.log('Hello ' + task.name);
callback();
}, 1);
// assign a callback
q.drain = function() {
console.log('All items have been processed');
};
// add some items to the queue with priority
q.push({name: 'foo'}, 3, function(err) {
console.log('Finished processing foo');
});
q.push({name: 'bar'}, 2, function (err) {
console.log('Finished processing bar');
});
// add some items to the queue (batch-wise) which will have same priority
q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], 1, function(err) {
console.log('Finished processing item');
});
6. Race
It runs all the tasks in parallel, but as soon as any of the function completes its execution or passes error to its callback, the main callback is immediately called.
async.race(tasks, callback)
Task: Here, it is a collection of functions to run. It is an array or any iterable.
Callback: The result of the first complete execution is passed. It may be the result or error.
An example of Race is shared below:
async.race([
function (callback) {
setTimeout(function () {
callback(null, 'one');
}, 300);
},
function (callback) {
setTimeout(function () {
callback(null, 'two');
}, 100);
},
function (callback) {
setTimeout(function () {
callback(null, 'three');
}, 200);
}
],
// main callback
function (err, result) {
// the result will be equal to 'two' as it finishes earlier than the other 2
console.log('The result is ', result);
});
Combining Async Flows
In complex scenarios, the async flows like parallel and series can be combined and nested. This helps in achieving the expected output with the benefits of async utilities.
However, the only difference between Waterfall and Series async utility is that the final callback in series receives an array of results of all the task whereas in Waterfall, the result object of the final task is received by the final callback.
Conclusion
Async Utilities has an upper hand over promises due to its concise and clean code, better error handling and easier debugging. It makes us realize how simple and easy asynchronous code can be without the syntactical mess of promises and callback hell.



