Why forEach Is Losing Favor: Switch to for...of for Safer Async Loops

The article explains why Array.prototype.forEach is increasingly discouraged in modern JavaScript, especially when combined with async/await, and shows how replacing it with a for...of loop resolves asynchronous pitfalls, enables proper flow‑control statements, and improves code clarity.

JavaScript
JavaScript
JavaScript
Why forEach Is Losing Favor: Switch to for...of for Safer Async Loops

If you follow popular open‑source projects, you may notice a trend: code reviews increasingly discourage Array.prototype.forEach and recommend replacing it with a for...of loop.

1. Asynchronous code pitfalls

The main reason to abandon forEach today is that, with async/await becoming the standard for asynchronous programming, forEach no longer fits. Its callback runs in a separate function scope and does not wait for asynchronous operations; it executes all iterations synchronously without pausing for any Promise.

Typical error example:

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const ids = [1, 2, 3];

async function processUsers(ids) {
  console.log('开始处理...');
  
  ids.forEach(async (id) => {
    await sleep(1000); // expect 1‑second wait per iteration
    console.log(`处理完用户 ${id}`);
  });
  
  console.log('...全部处理完毕');
}

processUsers(ids);

// Actual output (approximately after 1 second, almost simultaneous):
// 开始处理...
// ...全部处理完毕
// 处理完用户 1
// 处理完用户 2
// 处理完用户 3

The log "...全部处理完毕" is printed immediately, showing that forEach does not wait for the internal await. It merely triggers three parallel sleep operations, contrary to the intended sequential processing.

Solution: use for...of

The for...of loop pairs perfectly with await. Being a true loop construct rather than a function call, await can pause the entire loop until the Promise resolves.

2. Cannot break or skip the loop

Another native limitation of forEach is the inability to use break or continue as you can with a traditional for loop. Returning from the callback only exits that callback (similar to continue) and does not terminate the whole loop. To force a break you would need to throw an exception, which is generally discouraged.

Example attempting to break at num === 3:

const numbers = [1, 2, 3, 4, 5];

numbers.forEach(num => {
  if (num === 3) {
    // Want to stop the loop here, but cannot! return only skips this iteration
    return;
  }
  console.log(num);
});
// Output: 1, 2, 4, 5

In contrast, for...of fully supports break, continue, and other flow‑control keywords, making the logic clearer and more intuitive.

That said, forEach is not completely obsolete. It remains useful when operations are entirely synchronous, when you do not need to break or skip, and when its chainable syntax keeps code concise. It also works well as the final step in a method chain, e.g., after filter or map:

['error', 'warning', 'info'].forEach(level => console.log(level));
JavaScriptasync/awaitarray methodsforeachloopfor...of
JavaScript
Written by

JavaScript

Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.