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.
const users = ["Ada", "Grace", "Katherine"]
users.forEach((name, index) => {
console.log(`${index}: ${name}`)
})
// 0: Ada
// 1: Grace
// 2: KatherineThe callback takes three arguments
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:
["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:
// 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:
// 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:
// 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
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) // 3for...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:
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 loopBuild 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, orawait.
Reaching for forEach and pushing into an array declared above it is almost always map or filter written the long way.


