![Three neon pipelines comparing JavaScript array loops — for and forEach passing [1,2,3,4,5] through unchanged while map transforms it into [2,4,6,8,10]](/_next/image?url=%2Fimages%2Fblog_js_array_loops.png&w=3840&q=75)
2026-08-26
JavaScript Array Loops: for vs forEach vs map
Which JavaScript array loop should you use? A decision table for for, for...of, forEach and map, the async bug forEach causes, and a real benchmark.
Use map when you want a new array, filter when you want fewer items, for...of when you need to break or await, and a plain for loop only when you are looping over millions of items and have measured that it matters. forEach sits in an awkward middle: it reads nicely, but it cannot break and it silently ignores await — which makes it the wrong default more often than people realise.
If you're just starting out with arrays, our hands-on JavaScript arrays and loops tutorial walks through the basics first. This guide is the next step: which loop to reach for, and why.
Which JavaScript array loop should I use?
| You want to... | Use | Why |
|---|---|---|
| Transform every item into something new | map | Returns a new array, same length |
| Keep only some items | filter | Returns a new, shorter array |
| Boil the array down to one value | reduce | Sum, total, grouped object |
Stop early / break | for...of | forEach cannot be stopped |
await something each time round | for...of | forEach does not wait |
| Just do a side effect (log, push, update DOM) | for...of or forEach | Both fine; for...of is safer |
| Squeeze out speed on a huge array | for | Measurably faster, but see the benchmark below |
What is the difference between forEach and map?
map builds and returns a new array. forEach returns undefined — it exists purely for side effects.
const nums = [1, 2, 3, 4, 5];
const doubled = nums.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(nums); // [1, 2, 3, 4, 5] ← original untouched
console.log(nums.forEach(n => n * 2)); // undefined
The practical rule: if you're not using the returned array, you shouldn't be using map. Calling map purely for its side effects builds a whole array that you then throw away, and it misleads the next person reading the code into looking for a result that never gets used.
Its siblings work the same way and are often what you actually wanted:
const nums = [1, 2, 3, 4, 5];
nums.filter(n => n % 2 === 0); // [2, 4]
nums.reduce((sum, n) => sum + n, 0); // 15
Why can't I break out of forEach?
Because forEach doesn't offer a way to stop. A return inside the callback only ends that one call — the loop keeps going:
const nums = [1, 2, 3, 4, 5];
let visited = 0;
nums.forEach(n => {
visited++;
if (n > 3) return; // looks like a break, isn't one
});
console.log(visited); // 5 — every item was still visited
With for...of, break does what you expect:
let found = null;
for (const n of nums) {
if (n > 3) { found = n; break; }
}
console.log(found); // 4 — stopped as soon as it found one
If you only need to know whether something matches, the array methods built for the job are clearer than either: find, some, every and includes all stop as soon as they have their answer.
Why doesn't forEach work with async/await?
Because forEach does not wait for an async callback — the loop finishes long before any of the work does. This is the one that costs real debugging hours:
async function withForEach(ids) {
const results = [];
ids.forEach(async (id) => {
results.push(await fetchThing(id)); // runs later, too late
});
return results; // [] ← empty, every time
}
It returns an empty array. No error, no warning — forEach fires off five promises, ignores all of them, and returns. Use for...of, which genuinely pauses at each await:
async function withForOf(ids) {
const results = [];
for (const id of ids) {
results.push(await fetchThing(id)); // waits properly
}
return results; // [1, 2, 3, 4, 5]
}
That version runs the requests one after another. If they're independent and you want them in parallel, don't loop at all — map to an array of promises and await them together:
const results = await Promise.all(ids.map(id => fetchThing(id)));
Note the difference in intent: for...of with await is sequential (use it when each step depends on the last, or when you're being gentle on a rate-limited API), while Promise.all is concurrent and finishes as fast as the slowest request.
How do I get the index?
A plain for loop has it built in. With for...of, use entries():
const nums = [1, 2, 3, 4, 5];
for (const [i, n] of nums.entries()) {
console.log(i, n); // 0 1 / 1 2 / 2 3 ...
}
forEach and map pass the index as the second argument:
nums.forEach((n, i) => console.log(i, n));
nums.map((n, i) => `${i}: ${n}`);
Is a for loop actually faster?
Yes — and far less often than people assume. Here is a real measurement, summing an array of 10 million numbers on Node v24:
| Loop | Time |
|---|---|
for | 88 ms |
for...of | 577 ms |
forEach | 750 ms |
So a classic for loop is roughly 8× faster than forEach at that size. That sounds dramatic until you scale it down: for an array of 1,000 items — which covers the overwhelming majority of real code — the same gap is well under a millisecond. Nobody will ever notice it.
Treat these numbers as a rough shape rather than a law: results shift with the JavaScript engine, the version and what the callback actually does. Write the clearest loop first. Reach for a plain for when you have measured a real bottleneck on a genuinely large array, not because a benchmark article said it was faster.
What happens with sparse arrays?
Arrays can have gaps in them, and the loops disagree about what to do with those gaps:
const sparse = [1, , 3]; // note the missing middle item
let a = 0; sparse.forEach(() => a++);
let b = 0; for (const _ of sparse) b++;
console.log(a, b); // 2 3
forEach (and map, and filter) skip the hole; for...of visits it as undefined. This rarely comes up, but when it does it's a genuinely confusing bug — two loops over the same array producing different counts.
Frequently asked questions
Should I use forEach or for...of?
Prefer for...of as your default for side effects. It supports break and continue, it works correctly with await, and it handles sparse arrays predictably. forEach is fine for short, simple, synchronous callbacks — just never reach for it when async is involved.
Does map change the original array?
No. map returns a new array and leaves the original alone, which is why it fits well with React state and other code that expects immutable updates. Be aware the items aren't deep-copied though: if your array holds objects, both arrays still point at the same objects, so mutating one is visible from the other.
How do I loop over an array of objects from an API?
Exactly the same way — map to reshape them, filter to narrow them down, for...of when you need to await inside. If the JSON is hard to read while you're working out the shape, paste it into our JSON Formatter first.
What about for...in?
Avoid it for arrays. for...in loops over keys, gives you strings ("0", "1") rather than numbers, and includes inherited enumerable properties. It's meant for plain objects. For arrays, use for...of or the array methods.
The short version: pick the loop that says what you mean. map announces "I'm building a new array", filter announces "I'm narrowing this down", and for...of announces "I'm doing something for each item, and I might stop or wait." Speed almost never decides it — clarity does.
Want to practise on something real? Our digital clock tutorial puts loops and the Date API together in a small project you can build in an afternoon, and the JavaScript functions deep dive covers the callback syntax these array methods lean on.