Ch. 1 · JavaScript

map vs forEach vs for...of in JavaScript: Which Loop to Use

map builds a new array, forEach runs side effects and returns undefined, for...of can break. See sparse arrays, chaining and the parseInt trap.

~6 min readbeginnerupdated Oct 20, 2026

Short version: use map when you want a new array built from the old one, and use forEach when you want to do something for each item and don’t need anything back. forEach always returns undefined. When you need to stop early, await inside the loop, or iterate something that isn’t an array, reach for for...of.

Interviewers ask this because the wrong choice usually still “works”. Code that pushes into an outer array from forEach, or calls map and throws the result away, runs fine and passes review in a hurry. What they want to hear is that you pick the method that says what the code means.

How the three loops compare

map forEach for...of
Returns New array, same length undefined Nothing (it’s a statement)
Purpose Transform values Side effects General iteration
Chainable (.filter, .reduce) Yes No No
break / continue No No (return only skips the current item) Yes
Waits for await between items No No Yes, inside an async function
Works on Arrays (and typed arrays) Arrays, Map, Set, NodeList Any iterable: arrays, strings, Map, Set, generators
Holes in sparse arrays Skipped, holes kept in the output Skipped Visited as undefined
Callback gets index Yes, (value, index, array) Yes Use array.entries()

The return value is the whole difference

Run both on the same array in Node 22:

const prices = [10, 25, 40];
const a = prices.map((p) => p * 1.2);
const b = prices.forEach((p) => p * 1.2);
console.log(a, b);
JavaScript
[ 12, 30, 48 ] undefined
Text

map collects whatever the callback returns, in order, into a new array. The original prices array is untouched. forEach calls the same callback and discards the return value, which is why b is undefined. It isn’t a bug in forEach; it just has no result to give.

That leads to two common mistakes, one in each direction.

Using forEach to build an array. This works:

const withTax = [];
prices.forEach((p) => withTax.push(p * 1.2));
JavaScript

It produces the same [12, 30, 48], but you’ve written three moving parts (an empty array, a closure that mutates it, a push) where map needs one expression. Reviewers flag it because the reader has to trace the mutation to learn the shape of the result.

Using map for side effects. The reverse also runs:

const res = prices.map((p) => { console.log('saving', p); });
console.log(res);
JavaScript
saving 10
saving 25
saving 40
[ undefined, undefined, undefined ]
Text

The braces mean the arrow function has a body with no return, so every slot is undefined. You’ve allocated an array nobody wants, and anyone reading map expects a transformation. If the point is logging or saving, forEach or for...of says so.

Because forEach returns undefined, chaining after it fails at runtime:

TypeError: Cannot read properties of undefined (reading 'filter')
Text

That’s the error you get from prices.forEach(...).filter(...). With map, chaining is the normal style: prices.map((p) => p * 2).filter((p) => p > 30) gave [ 50, 80 ].

Stopping early: only for…of can break

There’s no way to break out of forEach or map. A return inside the callback ends that one call and moves on to the next item:

for (const p of prices) { if (p > 20) break; console.log('for...of saw', p); }
prices.forEach((p) => { if (p > 20) return; console.log('return only skips', p); });
JavaScript
for...of saw 10
return only skips 10
Text

The output looks identical here, but forEach still called the callback for 25 and 40. With a million items, or a callback that does real work, that difference matters. Throwing an exception does stop forEach (the error “stop at 25” was printed when I tried it), but using exceptions for control flow is a smell interviewers will call out.

If you’re searching, the right tool is often neither loop. users.find((u) => !u.active) returns the first match and stops, and some/every short-circuit too. Mentioning those shows you know the rest of the array API.

Holes, mutation and the parseInt trap

Sparse arrays. [1, , 3] has a hole at index 1, not an undefined value. map skips the callback for holes and keeps the hole in the output (Node prints [ 2, <1 empty item>, 6 ]). forEach ran its callback twice, not three times. for...of uses the array iterator, which reads every index, so it ran three times and logged undefined for the hole. You won’t meet holes often, but they appear with new Array(5) and delete arr[i], and they explain why new Array(3).map(() => 0) gives you three empty slots instead of zeros (use Array.from({ length: 3 }, () => 0)).

Adding items mid-loop. forEach and map decide how many indexes to visit before the first callback runs. Pushing 99 onto the array during a forEach left the array as [ 1, 2, 3, 99 ], but the callback never saw 99. A for...of loop over an array checks the length on each step, so it would keep going, and an unconditional push would make it loop forever.

map(parseInt). This is a favourite puzzle:

console.log(['1', '7', '11'].map(parseInt));
console.log(['1', '7', '11'].map(Number));
JavaScript
[ 1, NaN, 3 ]
[ 1, 7, 11 ]
Text

map passes three arguments: value, index and array. parseInt takes two: string and radix. So the calls are parseInt('1', 0) (radix 0 means “guess”, giving 1), parseInt('7', 1) (radix 1 is invalid, giving NaN) and parseInt('11', 2) (binary 11 is 3). Number ignores extra arguments. The lesson is wider than parseInt: passing a function reference straight into map passes the index too.

A realistic example: building select options

Say an API returns users and you need dropdown options for the active ones, plus a lookup table by id:

const users = [
  { id: 1, first: 'Ada', last: 'Lovelace', active: true },
  { id: 2, first: 'Alan', last: 'Turing', active: false },
  { id: 3, first: 'Grace', last: 'Hopper', active: true },
];
const options = users
  .filter((u) => u.active)
  .map((u) => ({ value: u.id, label: `${u.first} ${u.last}` }));

const byId = new Map();
users.forEach((u) => byId.set(u.id, u));
JavaScript

options came out as [{ value: 1, label: 'Ada Lovelace' }, { value: 3, label: 'Grace Hopper' }], and byId.get(3).first was 'Grace'. Each method matches its job: filter and map derive a new array (this is also exactly how a React component renders a list), while forEach fills a structure that already exists. You could also write new Map(users.map((u) => [u.id, u])), which avoids the mutation entirely.

On performance: a plain for loop can be faster in micro-benchmarks, but for everyday array sizes the difference rarely shows up in a profile. Pick for clarity first and measure before swapping loops for speed.

Async callbacks change the answer

map with an async callback returns an array of promises ([ Promise { 20 }, Promise { 50 }, Promise { 80 } ] in the test run), which you can pass to Promise.all to run the work in parallel. forEach with an async callback fires every call and doesn’t wait for any of them, so code after the loop runs too early. for...of with await runs items one at a time. The details, including limiting concurrency, are in async/await in loops.

Interview exercise: spot the problem

A candidate writes this to get the names of users over 18:

const names = [];
users.map((u) => { if (u.age > 18) names.push(u.name); });
JavaScript

What’s wrong, and how would you rewrite it?

Answer

It produces the right names, so the problem is intent, not output. map builds and returns an array that’s thrown away (full of undefined), and the real result comes from a side effect on an outer variable. A reader sees map and expects a transformation of every user, which isn’t what happens. The idiomatic version is const names = users.filter((u) => u.age > 18).map((u) => u.name);, which needs no outer state and no mutation. If performance on huge arrays mattered, a single for...of loop that pushes would avoid the intermediate array, and that’s a reasonable trade to mention as long as you say why.

Keep going

More in JavaScript

read ✓JavaScript · easy

JavaScript Array Sort Comparators

Sort arrays in place with a correct comparator, understand default string ordering and sort stability, and avoid the numeric and string traps.

~3 min readread →
read ✓JavaScript · easy

JavaScript Array Copies and Shared Objects

Why [...arr] and slice() copy the array but not the objects inside it, and how to choose a targeted update, structuredClone or JSON safely.

~7 min readread →
esc