NodeJS
Async.parallel
MongoDB
asynchronous programming
JavaScript

Using Async.parallel my parameter's life-span isn't outliving the asynchronous calls in NodeJS using MongoDB

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When using async.parallel with MongoDB in Node.js, variables declared in the outer scope can appear stale or undefined inside the parallel callbacks. This happens because JavaScript closures capture variable references, not values. If the variable changes before the async callback fires, the callback sees the changed value. The fix is to use let (block scoping), const, or pass values as function arguments to freeze them at capture time.

The Problem

javascript
1const async = require('async');
2const MongoClient = require('mongodb').MongoClient;
3
4const items = ['apple', 'banana', 'cherry'];
5const tasks = [];
6
7// BUG: 'i' and 'item' change before callbacks run
8for (var i = 0; i < items.length; i++) {
9    var item = items[i];
10    tasks.push(function(callback) {
11        db.collection('fruits').findOne({ name: item }, function(err, doc) {
12            console.log(`Looking for: ${item}`);  // Always "cherry"
13            callback(err, doc);
14        });
15    });
16}
17
18async.parallel(tasks, function(err, results) {
19    console.log(results);  // Three lookups for "cherry"
20});

All three callbacks print "cherry" because var item is function-scoped. By the time the callbacks execute, the loop has finished and item holds the last value.

Fix 1: Use let Instead of var

javascript
1const items = ['apple', 'banana', 'cherry'];
2const tasks = [];
3
4for (let i = 0; i < items.length; i++) {
5    const item = items[i];  // block-scoped — unique per iteration
6    tasks.push(function(callback) {
7        db.collection('fruits').findOne({ name: item }, function(err, doc) {
8            console.log(`Looking for: ${item}`);  // Correct value
9            callback(err, doc);
10        });
11    });
12}
13
14async.parallel(tasks, function(err, results) {
15    console.log(results);
16});

let and const create a new binding for each loop iteration. Each callback captures its own item.

Fix 2: Use Array.map

javascript
1const items = ['apple', 'banana', 'cherry'];
2
3const tasks = items.map(function(item) {
4    return function(callback) {
5        db.collection('fruits').findOne({ name: item }, function(err, doc) {
6            console.log(`Looking for: ${item}`);
7            callback(err, doc);
8        });
9    };
10});
11
12async.parallel(tasks, function(err, results) {
13    console.log(results);
14});

map creates a new function scope for each element. The item parameter is unique to each iteration.

Fix 3: Use IIFE (Immediately Invoked Function Expression)

For legacy code that must use var:

javascript
1for (var i = 0; i < items.length; i++) {
2    (function(item) {
3        tasks.push(function(callback) {
4            db.collection('fruits').findOne({ name: item }, callback);
5        });
6    })(items[i]);  // Pass current value immediately
7}

The IIFE creates a new scope, freezing the value of item at the time of invocation.

Fix 4: Use Promises and Promise.all (Modern Approach)

Replace async.parallel entirely with native Promises:

javascript
1const { MongoClient } = require('mongodb');
2
3async function findAll(items) {
4    const client = await MongoClient.connect('mongodb://localhost:27017');
5    const db = client.db('mydb');
6
7    const promises = items.map(item =>
8        db.collection('fruits').findOne({ name: item })
9    );
10
11    const results = await Promise.all(promises);
12    console.log(results);
13
14    await client.close();
15    return results;
16}
17
18findAll(['apple', 'banana', 'cherry']);

Arrow functions and async/await eliminate the closure issue entirely because each item is a parameter of the map callback.

Fix 5: Use async/await with for...of

javascript
1async function findSequentially(items) {
2    const results = [];
3    for (const item of items) {
4        const doc = await db.collection('fruits').findOne({ name: item });
5        results.push(doc);
6    }
7    return results;
8}

This runs queries sequentially (not in parallel), but each item is properly scoped. Use Promise.all with map for parallel execution.

Understanding var vs let Scoping

javascript
1// var — function-scoped, hoisted
2for (var i = 0; i < 3; i++) {
3    setTimeout(() => console.log(i), 100);
4}
5// Output: 3, 3, 3
6
7// let — block-scoped, new binding per iteration
8for (let i = 0; i < 3; i++) {
9    setTimeout(() => console.log(i), 100);
10}
11// Output: 0, 1, 2

This same principle applies to async.parallel, MongoDB callbacks, and any other asynchronous operation inside a loop.

MongoDB Connection Considerations

javascript
1// Share connection across parallel tasks — do NOT open per task
2const client = await MongoClient.connect(url, { maxPoolSize: 10 });
3const db = client.db('mydb');
4
5const tasks = items.map(item => callback => {
6    db.collection('fruits').findOne({ name: item }, callback);
7});
8
9async.parallel(tasks, (err, results) => {
10    // All tasks share the same connection pool
11    client.close();
12});

Common Pitfalls

  • Using var in loops with async callbacks: var is function-scoped, so all callbacks share the same variable. Always use let or const in loops containing async operations.
  • Mutating the array during iteration: If you modify items while async.parallel is running, callbacks may see changed values. Create a copy with [...items] if the source array might change.
  • Opening new MongoDB connections per task: Each async.parallel task should reuse a shared connection pool, not call MongoClient.connect() individually. Connection overhead dominates small queries.
  • Missing error handling in callbacks: If one parallel task fails, async.parallel calls the final callback with the error immediately. Remaining in-flight tasks still complete but their results are ignored.
  • Using async.parallel when Promise.all suffices: The async library was essential before native Promises. Modern Node.js (12+) supports async/await natively — prefer Promise.all over async.parallel in new code.

Summary

  • Variables declared with var are function-scoped and shared across loop iterations — async callbacks see the final value
  • Use let or const for block scoping so each callback captures its own value
  • Array.map naturally creates a new scope for each element
  • Modern Node.js should use Promise.all with async/await instead of the async library
  • Share MongoDB connections across parallel tasks using a connection pool

Course illustration
Course illustration

All Rights Reserved.