Development

JavaScript forEach(): what it does, and when not to use it

forEach runs a function for each element and returns nothing. The important part is what it cannot do — break early, await properly, or skip holes the way you expect.

forEach() calls a function once for each element of an array, in order, and returns undefined. It exists to perform side effects, not to produce a value.

bash
const users = ["Ada", "Grace", "Katherine"]

users.forEach((name, index) => {
  console.log(`${index}: ${name}`)
})
// 0: Ada
// 1: Grace
// 2: Katherine

The callback takes three arguments

bash
array.forEach((element, index, wholeArray) => { ... })

The third is rarely needed, but it is why passing a function that takes more than one parameter can misfire. The classic example:

bash
["1", "2", "3"].map(parseInt)        // [1, NaN, NaN]

parseInt accepts a radix as its second argument, and map supplies the index — so it parses "2" in base 1 and "3" in base 2. Use .map(Number) or .map(s => parseInt(s, 10)).

You cannot break out of forEach

This is the most common reason to choose something else:

bash
// Does not work — break is a syntax error here
items.forEach(item => {
  if (item.id === target) break
})

// Works
for (const item of items) {
  if (item.id === target) break
}

return inside the callback exits that one call, not the loop — the equivalent of continue, not break. If you need to stop early, use for...of, or pick the method that expresses the intent: find, some or every, all of which stop as soon as they can.

forEach does not await

This is the bug that reaches production:

bash
// Does NOT wait. Every call starts, the loop finishes immediately,
// and "done" prints before any save completes.
records.forEach(async (r) => {
  await save(r)
})
console.log("done")

forEach ignores the promise its callback returns. Use for...of to run sequentially, or Promise.all to run concurrently and still wait:

bash
// Sequential — one at a time, in order
for (const r of records) {
  await save(r)
}

// Concurrent — all at once, waits for all of them
await Promise.all(records.map(r => save(r)))

Choose deliberately. Concurrent is faster; sequential avoids overwhelming a database or hitting a rate limit.

Holes are skipped

bash
const sparse = [1, , 3]     // a hole at index 1
sparse.forEach(v => console.log(v))   // 1, 3 — the hole is not visited
console.log(sparse.length)            // 3

for...of visits the hole and yields undefined. Sparse arrays are rare in ordinary code, but new Array(5) creates one — which is why new Array(5).forEach(...) appears to do nothing at all.

Mutating while iterating

The range is fixed before the first call, so appending during iteration does not extend the loop — but removing does cause elements to be skipped:

bash
const xs = [1, 2, 3, 4]
xs.forEach((v, i) => { if (v % 2) xs.splice(i, 1) })
// leaves [2, 4] only by luck; the indexes shift underneath the loop

Build a new array with filter instead of removing in place.

Choosing between the options

  • forEach — a side effect for every element, no early exit, no awaiting.
  • map — you want a new array of the same length.
  • filter — you want a subset.
  • reduce — you want a single value.
  • find / some / every — you want one match or a boolean, and you want to stop early.
  • for...of — you need break, continue, or await.

Reaching for forEach and pushing into an array declared above it is almost always map or filter written the long way.

Arslan ud Din Shafiq

Founder and lead editor of LearnCybers. Full-stack engineer with expertise in Linux systems, cybersecurity, cloud infrastructure and web development. Writing about practical technology since 2019.

Related reading

Newsletter

Get smarter about security

Practical guides, tooling notes and the developments actually worth your attention — delivered when there is something worth saying.

No spam. Unsubscribe in one click.