Node.js async/await not waiting inside loop when calling database queries

## Summary
The issue occurred because an asynchronous loop using `Array.forEach` didn't wait for database queries to resolve before sending an API response. Despite using `await` inside the callback, `forEach` doesn't support asynchronous execution control, causing показd-related operations to run in the background after the response was sent.

## Root Cause
- **`forEach` ignores asynchronous operations**: Using `async/await` in `Array.forEach` creates multiple unawaited promises. The loop initiates all database calls but immediately exits because:
  - `forEach` doesn't return a promise
  - No mechanism exists to pause execution between iterations
- **פיתרון processing**: The `res.json()` call executes immediately after `forEach` schedules database work in the event loop queue, before any database results attach to the users

## Why This Happens in Real Systems
- **Concurrency misunderstandings**: Junior developers expect `await` to pause execution at every point it's used, but context matters:
  - `await` only affects the immediate `async` function scope
  - Looping constructs like `forEach` create separate `async` contexts that aren't coordinated
- **Database workflows are asynchronous at scale**: Real-world systems frequently iterate over fetched data to fetch related entities (e.g., orders for users). Naïve loop usage causes partial data responses when dependencies aren't properly chained.

## Real-World Impact
- **Inconsistent API behavior**: Clients sometimes receive incomplete or empty nested data arrays
- **Bugs requiring contextual replication**: Production logging may show successful database calls without revealing correlation failures
- **Resource starvation**: Uncontrolled parallelization (if N is large) can overwhelm database connections

## Example or Code
```javascript
// Incorrect approach (using forEach)
users.forEach(async (user) => {
  // This creates unresolved promises
  user.orders = await getOrdersByUserId(user.id); 
});

// Correct approach 1: map with Promise.all
const userWithOrders = await Promise.all(users.map(async (user) => {
  const orders = await getOrdersByUserId(user.id);
  return { ...user, orders };
}));
res.json(userWithOrders);

// Correct approach 2: for...of loop
for (const user of users) {
  user.orders = await getOrdersByUserId(user.id);
}
res.json(users);

How Senior Engineers Fix It

  1. Avoid forEach for async: Always use explicit control-flow constructs that support await
  2. Leverage promise aggregation: When feasible, process operations concurrently using:
    • Promise.all() for maximum parallelism
    • Promise.allSettled() for partial success handling
  3. Sequential execution when dependency-sensitive: Use for...of for ordering-sensitive operations
  4. Add explicit flow checks:
    // Validate completion timing
    console.time('order-fetch');
    // ...async operations
    console.timeEnd('order-fetch');

Why Juniors Miss It

  • Intuitive misconceptions: Assuming callbacks with await will pause the outer function
  • Production/testing dissonance:
    • Local databases respond too quickly to reveal timing bugs
    • Production latency exposes escaping unresolved promises
  • Documentation gaps: Array.forEach documentation rarely highlights its incompatibility with async/await
  • Syntactic camouflage: Code “looks clean” and uses modern async/await syntax, creating false confidence

Leave a Comment