Summary
This incident stems from a common misunderstanding of how async functions behave inside array iteration methods. The engineer expected await inside forEach to serialize asynchronous calls, but forEach does not await anything. As a result, all asynchronous operations fired concurrently, producing unexpected log ordering.
Root Cause
The root cause is using await inside Array.forEach, which does not respect asynchronous flow.
Key points:
forEachdoes not await the callback.- The callback is invoked synchronously, launching all async operations immediately.
awaitonly pauses inside the callback, not the outer function.- The surrounding code continues executing, leading to:
- All requests being sent at once
finishlogging before any responses return
Why This Happens in Real Systems
Real systems frequently exhibit this behavior because:
- Array iteration helpers (
forEach,map,filter) are not async-aware - Engineers assume
awaitforces sequential execution, but it only pauses the current async function - Event-loop scheduling means logs and callbacks interleave in unintuitive ways
- Promises resolve independently, so completion order differs from invocation order
Real-World Impact
This misunderstanding can cause:
- Unintentional concurrency, overwhelming downstream services
- Race conditions, especially when order matters
- Misleading logs, complicating debugging
- Resource exhaustion, such as too many open connections
- Partial failures, where some requests succeed and others fail unpredictably
Example or Code (if necessary and relevant)
Below is the correct sequential version using a for...of loop:
console.warn('start');
for (const instance of instances) {
console.log('request sent');
const metrics = await collectMetrics(instance);
console.log('data received', metrics);
}
console.warn('finish');
And the correct concurrent version (if concurrency is desired):
console.warn('start');
const results = await Promise.all(
instances.map(instance => collectMetrics(instance))
);
console.log('data received', results);
console.warn('finish');
How Senior Engineers Fix It
Experienced engineers apply patterns that avoid async pitfalls:
- Use
for...offor sequential async operations - Use
Promise.allfor controlled concurrency - Never use
awaitinsideforEach - Wrap async flows in dedicated orchestration functions
- Add structured logging to clarify execution order
- Document async behavior to prevent future confusion
Why Juniors Miss It
Junior engineers often miss this issue because:
- They assume
awaitis a global pause, not a local pause - They expect array helpers to be async-aware, but they are not
- They lack experience with event-loop timing and microtask queues
- They rely on intuition rather than understanding Promise scheduling
- They have not yet internalized that async ≠ synchronous, even with
await
This misunderstanding is extremely common, and resolving it is a key milestone in mastering asynchronous JavaScript.