DEV Community

ValPetal Tech Labs
ValPetal Tech Labs

Posted on

Javascript # Question of the Day #12 [Talk::Overflow]

This post explains a quiz originally shared as a LinkedIn poll.

🔹 The Question

const ids = [1, 2, 3];

async function fetchData(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve(id * 10), 100);
  });
}

async function processAll() {
  const results = [];
  ids.forEach(async (id) => {
    const data = await fetchData(id);
    results.push(data);
  });
  console.log(results);
}

processAll();
Enter fullscreen mode Exit fullscreen mode

Hint: Think about whether forEach waits for async callbacks to complete before continuing to the next statement.

🔹 Solution

Correct Answer: B) []

The output is: []

🧠 How this works

This is one of the most common async/await pitfalls in JavaScript. The issue is that forEach does not wait for async callbacks to complete. It simply fires off each callback and immediately moves on.

Here's what actually happens:

  1. forEach is called with an async callback
  2. For each element, forEach invokes the async callback
  3. Each async callback returns a Promise immediately (without waiting for the await inside)
  4. forEach ignores these returned Promises entirely
  5. forEach completes synchronously, moving to console.log(results)
  6. At this point, all three fetchData calls are still pending
  7. console.log(results) prints [] because nothing has been pushed yet
  8. ~100ms later, all three Promises resolve and push to results, but nobody is watching anymore

The key insight: forEach was designed for synchronous operations. It doesn't understand Promises. When you pass an async function, it receives a Promise but does nothing with it—it doesn't await it.

🔍 Line-by-line explanation

  1. const ids = [1, 2, 3] — creates an array of IDs to process

  2. fetchData(id) — simulates an async operation that takes 100ms and returns id * 10

  3. processAll() is called:

    • const results = [] — creates empty results array
    • ids.forEach(async (id) => {...}) starts:
      • Iteration 1: Calls async (1) => {...}, gets Promise back, ignores it
      • Iteration 2: Calls async (2) => {...}, gets Promise back, ignores it
      • Iteration 3: Calls async (3) => {...}, gets Promise back, ignores it
      • All three iterations happen synchronously in microseconds
    • forEach completes (it didn't wait for any Promises)
    • console.log(results)[] (results is still empty)
  4. ~100ms later (after console.log already ran):

    • First Promise resolves, pushes 10 to results
    • Second Promise resolves, pushes 20 to results
    • Third Promise resolves, pushes 30 to results
    • results is now [10, 20, 30], but we already logged

The misleading part: The async/await keywords make it look like each iteration will wait for fetchData to complete before moving on. But await only pauses execution within that async function—it doesn't pause forEach itself.

🔹 The Fix

Option 1: Use for...of for sequential processing

async function processAll() {
  const results = [];
  for (const id of ids) {
    const data = await fetchData(id);
    results.push(data);
  }
  console.log(results); // [10, 20, 30]
}
Enter fullscreen mode Exit fullscreen mode

Option 2: Use Promise.all with map for parallel processing

async function processAll() {
  const results = await Promise.all(
    ids.map(id => fetchData(id))
  );
  console.log(results); // [10, 20, 30]
}
Enter fullscreen mode Exit fullscreen mode

Option 3: Use for await...of with async iterables

async function processAll() {
  const results = [];
  for await (const data of ids.map(fetchData)) {
    results.push(data);
  }
  console.log(results); // [10, 20, 30]
}
Enter fullscreen mode Exit fullscreen mode

When to use which:

  • for...of with await: When you need sequential processing (each iteration waits for the previous)
  • Promise.all with map: When operations can run in parallel (faster, but all-or-nothing on errors)
  • Promise.allSettled: When you want parallel execution but need to handle individual failures

🔹 Key Takeaways

  • forEach does not await async callbacks—it fires and forgets
  • await only pauses the inner async function, not the outer forEach
  • For sequential async iteration, use for...of with await
  • For parallel async iteration, use Promise.all with map
  • This applies to other array methods too: map, filter, reduce all ignore returned Promises
  • When you see forEach(async ...), it's almost always a bug
  • ESLint rule no-await-in-loop can help catch this (though it flags the correct for...of pattern too)

Top comments (0)