Episode 1 — Fundamentals / 1.22 — Array Methods

1.22.d — forEach()

In one sentence: forEach() executes a function once for every element but returns undefined — it is designed purely for side effects, not for producing a new value.

Navigation: ← 1.22.c — reduce() · 1.22.e — some() and every() →


1. What forEach() does

forEach loops through every element and passes it to your callback. Unlike map or filter, it does not build a new array and does not return anything useful.

const fruits = ["apple", "banana", "cherry"];

fruits.forEach(fruit => {
  console.log(fruit.toUpperCase());
});
// APPLE
// BANANA
// CHERRY

const result = fruits.forEach(f => f.toUpperCase());
console.log(result); // undefined  ← always!

2. Syntax

arr.forEach(callback(element, index, array));
ParameterDescription
elementThe current element
index(optional) The current index
array(optional) The original array
ReturnAlways undefined — no matter what the callback returns
const colors = ["red", "green", "blue"];

colors.forEach((color, index, arr) => {
  console.log(`${index + 1}/${arr.length}: ${color}`);
});
// 1/3: red
// 2/3: green
// 3/3: blue

3. Cannot break or continue

Unlike a for loop, there is no way to exit a forEach early:

// THIS DOES NOT WORK
[1, 2, 3, 4, 5].forEach(n => {
  if (n === 3) break;   // SyntaxError: Illegal break statement
  console.log(n);
});

// THIS ALSO DOES NOT WORK (return only exits the current callback, not the loop)
[1, 2, 3, 4, 5].forEach(n => {
  if (n === 3) return;  // skips this iteration but does NOT stop the loop
  console.log(n);
});
// 1, 2, 4, 5  ← 3 was skipped, but 4 and 5 still run

If you need early exit, use for...of, some(), or every().


4. Cannot use await properly inside forEach

This is a very common bug:

// BUG: forEach does not await each callback
const urls = ["/api/1", "/api/2", "/api/3"];

urls.forEach(async (url) => {
  const res = await fetch(url);   // fires all at once, not sequentially
  const data = await res.json();
  console.log(data);
});
console.log("Done!");
// "Done!" prints BEFORE any fetch completes!

Why: forEach does not look at the return value of the callback. It gets a Promise back and ignores it.

Fix: use for...of:

async function fetchAll(urls) {
  for (const url of urls) {
    const res = await fetch(url);
    const data = await res.json();
    console.log(data);
  }
  console.log("Done!"); // actually runs after all fetches
}

5. forEach vs for...of

FeatureforEachfor...of
Break / continueNot possibleYes
await supportNo (fires and forgets)Yes (sequential await)
Return valueundefinedN/A (it is a statement)
ReadabilityCallback-styleLoop-style
Works onArrays (and array-likes with Array.from)Any iterable (arrays, strings, Maps, Sets)
Sparse arraysSkips holesVisits holes as undefined
PerformanceSlight overhead (function call per element)Marginally faster in tight loops

When to prefer for...of:

  • Need break, continue, or return (from enclosing function)
  • Need await inside the loop
  • Iterating non-array iterables (Map, Set, generators)

When to prefer forEach:

  • Simple side-effect on every element, no early exit needed
  • Stylistic preference for functional/callback style

6. forEach vs map — common mistake

// WRONG: using forEach to build an array
const doubled = [];
[1, 2, 3].forEach(n => doubled.push(n * 2));
// Works, but awkward — why manage the array yourself?

// RIGHT: use map
const doubled2 = [1, 2, 3].map(n => n * 2);
// WRONG: using map for a side effect (wasteful)
[1, 2, 3].map(n => console.log(n));
// Creates a useless array [undefined, undefined, undefined]

// RIGHT: use forEach for side effects
[1, 2, 3].forEach(n => console.log(n));

Decision rule:

SituationUse
Need the result arraymap
Just performing an action per elementforEach
Need early exitfor...of

7. Side-effect-oriented examples

Logging data

const users = [
  { name: "Alice", role: "admin" },
  { name: "Bob", role: "user" },
  { name: "Carol", role: "user" },
];

users.forEach(user => {
  console.log(`[${user.role.toUpperCase()}] ${user.name}`);
});
// [ADMIN] Alice
// [USER] Bob
// [USER] Carol

DOM updates

const items = ["Home", "About", "Contact"];
const nav = document.querySelector("nav");

items.forEach(item => {
  const link = document.createElement("a");
  link.textContent = item;
  link.href = `/${item.toLowerCase()}`;
  nav.appendChild(link);
});

Accumulating into an external data structure

const scores = new Map();

["Alice:85", "Bob:92", "Carol:78"].forEach(entry => {
  const [name, score] = entry.split(":");
  scores.set(name, Number(score));
});

console.log(scores);
// Map(3) { "Alice" => 85, "Bob" => 92, "Carol" => 78 }

Sending analytics events

const events = [
  { type: "click", target: "button#submit" },
  { type: "scroll", depth: 50 },
  { type: "click", target: "a.nav-link" },
];

events.forEach(event => {
  analytics.track(event.type, event);
});

8. Updating external state

forEach is appropriate when you need to modify something outside the callback:

let total = 0;
const prices = [19.99, 42.50, 8.99, 35.00];

prices.forEach(price => {
  total += price;
});

console.log(total); // 106.48
// Works, but reduce is more idiomatic for this:
// const total = prices.reduce((sum, p) => sum + p, 0);

9. forEach on array-like objects

forEach is an array method. NodeLists from querySelectorAll also have forEach, but older array-likes (like arguments) do not:

// NodeList has forEach (modern browsers)
document.querySelectorAll("p").forEach(p => {
  p.style.color = "blue";
});

// arguments does NOT have forEach — convert first
function logArgs() {
  // arguments.forEach(...);  // TypeError!
  Array.from(arguments).forEach(arg => console.log(arg));
}

10. Edge cases

// Empty array — callback never runs
[].forEach(n => console.log(n));
// (no output)

// Sparse arrays — skips holes
const sparse = [1, , 3];
sparse.forEach(n => console.log(n));
// 1
// 3  ← index 1 (the hole) is skipped entirely

// Mutating array during forEach — behavior is defined but confusing
const arr = [1, 2, 3, 4, 5];
arr.forEach((val, i) => {
  if (val === 3) arr.push(99);
  console.log(val);
});
// 1, 2, 3, 4, 5  ← 99 is NOT visited (length was fixed at start)

11. When forEach is the right choice

Use forEach when:

  1. You are performing a side effect (logging, DOM update, API call)
  2. You do not need the return values
  3. You do not need to break early
  4. You do not need await inside the loop

For everything else, there is usually a better method.


Key takeaways

  1. forEach() runs a callback on each element and always returns undefined.
  2. You cannot break, continue, or properly await inside forEach.
  3. Use forEach for side effects (logging, DOM, external state).
  4. Use map when you need a new array, reduce when you need a single value.
  5. Use for...of when you need early exit or await.
  6. Do not use map purely for side effects — it creates a wasted array.

Explain-It Challenge

Explain without notes:

  1. What does forEach return? Always?
  2. Why does await inside forEach not work as expected?
  3. A colleague wrote const names = users.forEach(u => u.name). What is wrong and how do you fix it?

Navigation: ← 1.22.c — reduce() · 1.22.e — some() and every() →