Node.js
MongoDB
asynchronous programming
loop completion
promises

nodejs wait until all MongoDB calls in loop finish

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

When MongoDB calls are made inside a loop in Node.js, the common mistake is to start all the async work and then immediately continue as if it has already finished. The fix is not "wait for the loop." It is to collect or await the promises created inside the loop. Once you do that, the code becomes predictable and much easier to reason about.

The Wrong Pattern

This pattern starts database operations but does not wait for them:

javascript
1ids.forEach(async (id) => {
2  const doc = await collection.findOne({ _id: id });
3  console.log(doc);
4});
5
6console.log("done");

forEach does not understand await in the way many people expect. The "done" line runs immediately, before the queries finish.

Use Promise.all for Parallel Work

If the MongoDB operations are independent and you want them to run concurrently, map each item to a promise and wait for all of them together.

javascript
1const { MongoClient, ObjectId } = require("mongodb");
2
3async function loadUsers(ids) {
4  const client = new MongoClient("mongodb://127.0.0.1:27017");
5
6  try {
7    await client.connect();
8    const collection = client.db("app").collection("users");
9
10    const promises = ids.map((id) =>
11      collection.findOne({ _id: new ObjectId(id) })
12    );
13
14    const users = await Promise.all(promises);
15    console.log(users);
16    return users;
17  } finally {
18    await client.close();
19  }
20}

The important detail is that ids.map(...) returns an array of promises. Promise.all(...) waits until every promise resolves or until one rejects.

Handle Errors Deliberately

Promise.all fails fast. If one MongoDB call rejects, the entire await Promise.all(...) throws. That is often correct, but not always.

If you want to collect both successes and failures, use Promise.allSettled:

javascript
1const results = await Promise.allSettled(
2  ids.map((id) => collection.findOne({ _id: new ObjectId(id) }))
3);
4
5for (const result of results) {
6  if (result.status === "fulfilled") {
7    console.log("doc:", result.value);
8  } else {
9    console.error("query failed:", result.reason);
10  }
11}

This is useful when one bad document ID should not discard all other results.

Use for...of for Sequential Work

Sometimes you do not want parallel queries. Maybe each query depends on the previous one, or maybe you need to limit database pressure. In that case, use for...of with await.

javascript
1async function loadSequentially(ids, collection) {
2  const users = [];
3
4  for (const id of ids) {
5    const user = await collection.findOne({ _id: new ObjectId(id) });
6    users.push(user);
7  }
8
9  return users;
10}

This runs one query at a time. It is slower than parallel execution, but the control flow is explicit and safe.

Avoid Opening a New Connection in Every Iteration

Another common mistake is connecting to MongoDB inside the loop. That creates unnecessary overhead and can exhaust connection limits.

Bad pattern:

javascript
1for (const id of ids) {
2  const client = new MongoClient(uri);
3  await client.connect();
4  // query
5  await client.close();
6}

Good pattern:

  • create one client
  • connect once
  • reuse the collection handle
  • close the client when all work is complete

The MongoDB driver is built for connection reuse. Use it that way.

Prefer Bulk Operations When Possible

If the loop is doing many inserts, updates, or deletes, waiting for all promises may still be the wrong design. MongoDB often provides a bulk operation that is more efficient and easier to reason about.

For example, instead of many separate inserts:

javascript
1await collection.insertMany([
2  { name: "Ada" },
3  { name: "Grace" },
4  { name: "Linus" }
5]);

Or instead of fetching one document at a time, fetch many at once when the query shape allows it:

javascript
const users = await collection
  .find({ _id: { $in: ids.map((id) => new ObjectId(id)) } })
  .toArray();

That is often better than launching dozens or hundreds of findOne calls.

Concurrency Limits Matter

Blindly firing thousands of MongoDB queries with Promise.all can overwhelm the application or the database. If the input list is large, limit concurrency with a queue or a utility library.

A simple chunking approach:

javascript
1async function processInBatches(ids, collection, size = 50) {
2  const results = [];
3
4  for (let i = 0; i < ids.length; i += size) {
5    const batch = ids.slice(i, i + size);
6    const batchResults = await Promise.all(
7      batch.map((id) => collection.findOne({ _id: new ObjectId(id) }))
8    );
9    results.push(...batchResults);
10  }
11
12  return results;
13}

This keeps concurrency under control without making the whole workload fully sequential.

Choose the Pattern Based on Intent

The right answer depends on the workload:

  • independent queries: Promise.all
  • independent queries with partial failure handling: Promise.allSettled
  • order-dependent or rate-limited queries: for...of with await
  • many similar write operations: use MongoDB bulk APIs

The mistake is not asynchronous programming itself. The mistake is using a loop construct that does not match the async semantics you need.

Common Pitfalls

  • Using forEach with async and expecting the outer code to wait automatically.
  • Opening and closing a MongoDB client inside every iteration instead of reusing one connection.
  • Using Promise.all for a huge list and overwhelming the database with uncontrolled concurrency.
  • Launching many single-document operations when one bulk query or bulk write would be more efficient.
  • Forgetting that Promise.all rejects as soon as one promise fails.

Summary

  • To wait for MongoDB calls in a loop, wait for the promises created by the loop, not the loop construct itself.
  • Use Promise.all for parallel independent operations.
  • Use Promise.allSettled when you need success and failure details for every operation.
  • Use for...of with await for sequential or rate-limited execution.
  • Reuse the MongoDB client and prefer bulk operations when the database can do the work more efficiently.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

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

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.