Three JavaScript loop styles — forEach, map and for...of — look interchangeable right up to the moment you try to use the result of one, or to stop early inside another. This lesson lines them up against the same data so you can pick by behaviour instead of by habit.
What does map give you that forEach does not?
map builds a new array out of whatever your callback returns. forEach runs your callback purely for its side effects and hands back undefined. If your callback computes a value and you then throw that value away, you used forEach where map was the right tool.
const users = [
{ name: 'Ada', role: 'admin' },
{ name: 'Linus', role: 'dev' },
];
const names = users.map((user) => user.name);
console.log(names); // [ 'Ada', 'Linus' ]
const roles = users.map((user) => user.role.toUpperCase());
console.log(roles); // [ 'ADMIN', 'DEV' ]
Neither version touches the original array. map returns a new array of the same length; forEach returns nothing to keep. That single difference is the fastest way to choose between them.
Why can you not break out of a forEach?
Because forEach has no way to be told to stop. break and continue are syntax errors inside the callback, and return only leaves the callback, not the loop. That makes forEach a poor fit for searching, where you want to stop at the first hit.
const rows = ['a', 'b'];
const result = rows.forEach((row) => row);
console.log(result); // undefined
const copied = rows.map((row) => row);
console.log(copied); // [ 'a', 'b' ]
The two console.log lines make the return-value difference concrete: forEach gave you undefined, map gave you a new array. When you need to stop at the first match, use find from the next lesson, or a for...of loop.
When is for-of the better choice?
for...of gives you the value inside a real loop, so break, continue and return all behave the way you expect. Reach for it when the loop body does more than transform one value — when it logs, calls a function, awaits something, or needs an early exit.
const labels = ['one', 'two', 'three'];
for (const label of labels) {
if (label === 'two') break;
console.log('kept:', label);
}
That prints kept: one and stops. Swap break for continue and the loop skips the rest of the body for that item but carries on with the next. Worth knowing: for...of works on anything iterable — strings, Sets, Maps, and the entries() helper — so it is not limited to arrays.
How do you get the index while looping?
map and forEach pass the index as the second argument to your callback. for...of does not, because it only yields values. When you need both, you have two options: use the callback's index, or iterate entries().
const pairs = ['x', 'y', 'z'];
pairs.forEach((value, index) => {
console.log(index, value);
});
for (const [index, value] of pairs.entries()) {
console.log(index, value);
}
Both loops print 0 x, 1 y, 2 z. entries() produces an iterator of [index, value] pairs, so for...of plus a destructuring pattern gives you both without a callback at all. The square-bracket pattern is destructuring, and the destructuring module later in this tutorial covers it properly — for now just read it as “take the first item as index, the second as value”.
Do any of them run faster?
For arrays of similar size the differences are small enough that you should choose on readability. Two practical notes still matter. map allocates a new array on every call, while forEach allocates nothing, so forEach is the cheaper choice when you only need to trigger an action. And chaining three methods such as filter().map().filter() means three separate passes over the data — perfectly fine for a few hundred items, worth collapsing into a single reduce once you are dealing with thousands.
Why does passing a function straight to map break?
The callback receives more arguments than you might expect: the value, the index, and the whole array. Passing a function that happens to accept a second parameter can therefore change its behaviour, and map(parseInt) is the classic example.
console.log(['1', '2', '3'].map(parseInt));
// [ 1, NaN, NaN ]
console.log(['1', '2', '3'].map((s) => parseInt(s, 10)));
// [ 1, 2, 3 ]
parseInt takes a second argument for the radix, so the index 1 becomes the base and parseInt('2', 1) is nonsense. The fix is to wrap it in an arrow function so only the value is passed. If a function you pass to map has a second parameter, check what that parameter means before you assume the result is about the array.
Which loop style should you reach for?
| Style | Use it when | Returns | Can stop early? |
|---|---|---|---|
forEach | You only need a side effect: logging, pushing, drawing | undefined | No |
map | You need a new array of transformed values | New array | No |
for...of | You need control flow, an index, or the source is not an array | Whatever your loop body produces | Yes, with break |
One tie-breaker settles most arguments. If you are writing a function that returns an array of transformed values, use map. If it returns nothing and performs an action, use forEach. If the body needs an early exit or the index, use for...of. And if the callback body needs a nested if just to skip work, you usually wanted filter or find instead.
Try it yourself: take an array of three products, each with name, price and stock. Write the same task three ways: print each product name with forEach, build an array of prices with map, and print only the products with stock above zero using for...of with continue. Then write discount(products, rate) so it returns a new array of objects with the same names and prices reduced by the given rate, using map, and print the original array afterwards to confirm none of its prices changed.