node.js
async.series
troubleshooting
asynchronous programming
JavaScript

node.js async.series not working

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

async.series from the async library runs an array of functions sequentially, passing results to a final callback. The most common reason it "does not work" is forgetting to call the callback parameter inside each function. If the callback is never called, async.series hangs and the final callback never fires. Other causes include passing the wrong callback signature, throwing errors instead of passing them to the callback, or mixing up async.series with async.parallel.

How async.series Works

javascript
1const async = require('async');
2
3async.series([
4    function(callback) {
5        // Task 1
6        setTimeout(() => {
7            console.log('Task 1 done');
8            callback(null, 'result1');  // MUST call callback
9        }, 1000);
10    },
11    function(callback) {
12        // Task 2 — runs AFTER Task 1 completes
13        setTimeout(() => {
14            console.log('Task 2 done');
15            callback(null, 'result2');  // MUST call callback
16        }, 500);
17    }
18], function(err, results) {
19    // Final callback — runs after all tasks complete
20    console.log('All done:', results);
21    // ['result1', 'result2']
22});

Each function receives a callback(err, result) parameter. Call it with null as the first argument on success, or an Error on failure. If any task passes an error, the remaining tasks are skipped and the final callback fires immediately with that error.

Problem 1: Not Calling the Callback

javascript
1// BROKEN — callback is never called, series hangs forever
2async.series([
3    function(callback) {
4        console.log('Task 1');
5        // Missing: callback(null, 'done');
6    },
7    function(callback) {
8        console.log('Task 2');  // Never reached
9        callback(null, 'done');
10    }
11], function(err, results) {
12    console.log('Final');  // Never reached
13});

Fix: Always call callback() in every code path:

javascript
1async.series([
2    function(callback) {
3        console.log('Task 1');
4        callback(null, 'done');  // Added
5    },
6    function(callback) {
7        console.log('Task 2');
8        callback(null, 'done');
9    }
10], function(err, results) {
11    console.log('Final:', results);  // ['done', 'done']
12});

Problem 2: Calling Callback Multiple Times

javascript
1// BROKEN — callback called twice causes unpredictable behavior
2async.series([
3    function(callback) {
4        doSomethingAsync(function(err, data) {
5            if (err) {
6                callback(err);
7                // BUG: no return — execution continues
8            }
9            callback(null, data);  // Called again!
10        });
11    }
12], function(err, results) {
13    // May be called multiple times
14});

Fix: Return after calling the callback, or use if/else:

javascript
1function(callback) {
2    doSomethingAsync(function(err, data) {
3        if (err) {
4            return callback(err);  // return prevents double call
5        }
6        callback(null, data);
7    });
8}

Problem 3: Throwing Instead of Passing Errors

javascript
1// BROKEN — thrown errors crash the process instead of reaching final callback
2async.series([
3    function(callback) {
4        const data = JSON.parse(invalidJson);  // throws SyntaxError
5        callback(null, data);
6    }
7], function(err, results) {
8    // Never reached — the error crashed the process
9});

Fix: Wrap in try/catch and pass the error to the callback:

javascript
1function(callback) {
2    try {
3        const data = JSON.parse(invalidJson);
4        callback(null, data);
5    } catch (err) {
6        callback(err);
7    }
8}

Problem 4: Using async.series With Promises

async.series is callback-based. If your functions return Promises, they will not work correctly:

javascript
1// BROKEN — async.series ignores the returned Promise
2async.series([
3    async function() {
4        const data = await fetchData();
5        return data;  // This return value is ignored
6    }
7], function(err, results) {
8    console.log(results);  // [undefined]
9});

Fix: Either use the callback parameter or switch to native Promise.all / for...of:

javascript
1// Option 1: Use callback with async functions
2async.series([
3    function(callback) {
4        fetchData()
5            .then(data => callback(null, data))
6            .catch(err => callback(err));
7    }
8]);
9
10// Option 2: Use native async/await instead (recommended)
11async function runTasks() {
12    const result1 = await fetchData();
13    const result2 = await processData(result1);
14    return [result1, result2];
15}

Problem 5: Wrong Callback Signature

javascript
1// BROKEN — callback expects (err, result), not just (result)
2async.series([
3    function(callback) {
4        callback('some result');  // First arg is treated as an error!
5    }
6], function(err, results) {
7    console.log(err);     // 'some result' — treated as error
8    console.log(results); // undefined
9});

Fix: Always pass null as the first argument on success:

javascript
callback(null, 'some result');  // null = no error

Modern Alternative: Native async/await

For new code, async/await replaces async.series entirely:

javascript
1// async.series equivalent with async/await
2async function runInSeries() {
3    const result1 = await task1();
4    const result2 = await task2();
5    const result3 = await task3();
6    return [result1, result2, result3];
7}
8
9runInSeries()
10    .then(results => console.log('Done:', results))
11    .catch(err => console.error('Error:', err));

This is simpler, requires no external library, and handles errors naturally with try/catch.

Common Pitfalls

  • Forgetting to call callback: The single most common issue. Every function in the series array must call callback() in every code path, including error paths and conditional branches.
  • Mixing async.series and async.parallel: async.parallel runs all tasks concurrently, async.series runs them sequentially. Using the wrong one produces unexpected timing behavior.
  • Not installing the async library: async.series requires npm install async. The built-in Node.js async/await is different from the async npm package.
  • Callback called after return in conditional: If an early if branch calls callback() without return, the code continues and calls callback() again at the end of the function.
  • Error handling: If one task passes an error to the callback, all remaining tasks are skipped. Make sure your final callback handles errors: if (err) { console.error(err); return; }.

Summary

  • async.series requires calling callback(null, result) in every function — forgetting this causes silent hangs
  • Pass errors as the first callback argument: callback(err), not by throwing
  • Never call the callback more than once — use return callback(err) to prevent double calls
  • For new projects, use native async/await instead of the async npm library
  • The callback signature is always callback(error, result)null first argument means success

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.