Knowledge Base/concepts/forEach vs map in JavaScript

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

  • forEach is for actions and returns undefined.
  • map is for transformations and returns a new array of the same length.
  • Do not use map if you ignore the returned array.
  • Do not use forEach to build a new array by pushing into it.
  • Use for...of for early exit or sequential async work; use Promise.all(ids.map(...)) for parallel async work.
Share this article:
javascriptfundamentalsarrays