CyberCodeLab logo — neon green lab flask with terminal symbolCyberCodeLab
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]

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...UseWhy
Transform every item into something newmapReturns a new array, same length
Keep only some itemsfilterReturns a new, shorter array
Boil the array down to one valuereduceSum, total, grouped object
Stop early / breakfor...offorEach cannot be stopped
await something each time roundfor...offorEach does not wait
Just do a side effect (log, push, update DOM)for...of or forEachBoth fine; for...of is safer
Squeeze out speed on a huge arrayforMeasurably 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:

LoopTime
for88 ms
for...of577 ms
forEach750 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.