forEach vs map in JavaScript
The difference between forEach and map in JavaScript is not style. forEach is for doing an action for each item. map is for creating a new array from an old one. If you ignore that distinction, the code may still run, but the next person reading it has to reverse-engineer your intention from side effects.
The return value gives the whole game away.
The simple rule
Use forEach when the result of the callback is not the point:
buttons.forEach((button) => {
button.addEventListener("click", handleClick);
});
Use map when each input item should become one output item:
const names = users.map((user) => user.name);
One is action-oriented. The other is transformation-oriented.
forEach returns undefined
forEach calls your callback once for each array element and then returns undefined.
const numbers = [1, 2, 3];
const result = numbers.forEach((n) => n * 2);
console.log(result); // undefined
The callback return value is ignored. That means this code does not create a doubled array:
const doubled = [];
numbers.forEach((n) => {
doubled.push(n * 2);
});
It works, but it is a manual rebuild of map with extra moving parts.
map returns a new array
map stores each callback return value in a new array:
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3]
The output array has the same length as the input array. If the callback forgets to return, the output still has the same length, but it is full of undefined:
const doubled = numbers.map((n) => {
n * 2;
});
console.log(doubled); // [undefined, undefined, undefined]
With braces, return explicitly:
const doubled = numbers.map((n) => {
return n * 2;
});
Or use a concise arrow body:
const doubled = numbers.map((n) => n * 2);
The JavaScript Array Methods guide covers map, filter, and reduce as a group.
Side effects belong in forEach
Side effects are actions that affect the world outside the callback: logging, DOM updates, analytics calls, event listeners, mutations, network requests.
const buttons = document.querySelectorAll("button");
buttons.forEach((button) => {
button.disabled = true;
});
Using map here would create an array you do not use:
buttons.map((button) => {
button.disabled = true;
});
That is a smell. map says "I am building a new array." If you are not using the new array, the method is lying on your behalf.
Transformations belong in map
If you are producing new data, use map:
const users = [
{ id: 1, firstName: "Ada", lastName: "Lovelace" },
{ id: 2, firstName: "Grace", lastName: "Hopper" },
];
const viewModels = users.map((user) => ({
id: user.id,
displayName: `${user.firstName} ${user.lastName}`,
}));
That is the clean shape: array in, array out, one output item per input item.
If you only want some items, use filter before or after map depending on the data you need:
const activeLabels = users
.filter((user) => user.active)
.map((user) => user.name);
Neither method breaks early
You cannot break out of forEach or map.
[1, 2, 3, 4].forEach((n) => {
if (n === 3) return;
console.log(n);
});
// 1
// 2
// 4
That return exits the callback for the current item. It does not stop the loop.
Use for...of when early exit is part of the logic:
for (const n of [1, 2, 3, 4]) {
if (n === 3) break;
console.log(n);
}
// 1
// 2
For value checks, find, some, and every can also stop early.
The async callback trap
This is the bug that keeps earning its rent:
ids.forEach(async (id) => {
await saveUser(id);
});
console.log("Done");
forEach does not wait for the async callback. It starts the callbacks and immediately moves on. "Done" can log before any save has finished.
For sequential async work, use for...of:
for (const id of ids) {
await saveUser(id);
}
console.log("Done");
For parallel async work, map is useful because it creates an array of Promises:
await Promise.all(ids.map((id) => saveUser(id)));
console.log("Done");
That is a legitimate map use: each id becomes one Promise, then Promise.all waits for the array.
Sparse arrays
Both forEach and map skip empty slots in sparse arrays:
const sparse = [1, , 3];
sparse.forEach((value) => {
console.log(value);
});
// 1
// 3
const mapped = sparse.map((value) => value * 2);
console.log(mapped);
// [2, <empty>, 6]
Most application arrays are not sparse, but it is useful to know this behavior when debugging odd data.
Key Takeaways
forEachis for actions and returnsundefined.mapis for transformations and returns a new array of the same length.- Do not use
mapif you ignore the returned array. - Do not use
forEachto build a new array by pushing into it. - Use
for...offor early exit or sequential async work; usePromise.all(ids.map(...))for parallel async work.