Iteration and array transformation · Lesson 31 of 77 · concept
forEach, and when a plain loop is clearer
About 55 minutes.
By the end of this lesson you can
- Write a forEach call that runs a function once for every element.
- Name the three arguments forEach passes to your function.
- Explain why break does not work inside forEach and what to use instead.
- Say what forEach returns, and why that stops it being chained.
- Decide between forEach and for...of for a given job, and defend the choice.
Handing the job to the array
Why it matters
Every loop so far has had the same shape: some machinery to walk the collection, wrapped around the bit that actually does the work. forEach inverts that. Instead of you walking the array, you hand the array a function and it does the walking, calling your function once for each element.
This is the first array method in the module, and the pattern it introduces is the whole rest of the module. map, filter, find, some, every and reduce all work the same way — you supply a small function, the method decides when and how often to call it. Get comfortable with the shape here, on the simplest of them, and the other six will feel familiar rather than magical.
A function you pass to another function like this is usually called a callback: you are not calling it yourself, you are handing it over to be called back later. Nothing mysterious is happening — it is an ordinary function value being passed as an argument, exactly as you saw in the functions module.
The shape of it
Explanation
You write array.forEach(callback). The callback is usually an arrow function written inline, because it is small and only used once. forEach calls it once per element, in order from the first to the last, and passes the element in as the first argument.
Read array.forEach((item) => { ... }) as: for each item in array, do this. That is close enough to English that a reader who has never seen the method can guess it correctly — which is a real advantage over a counting loop whose meaning has to be reconstructed from a header.
One caveat worth knowing rather than discovering: forEach skips holes. An array can have genuinely empty slots in the middle, written as [1, , 3], and forEach passes over them without calling your function, while a for...of loop visits them and hands you undefined. Holes are rare and best avoided, but this is why two loops over the same array can disagree about how many elements there are.
Three arguments, and you take only what you need
Explanation
forEach passes your callback three arguments in this order: the element, its index, and the whole array. You do not have to accept all three. A function that declares one parameter simply ignores the rest — that is ordinary JavaScript, not a special rule for callbacks.
The index is there for the cases where you do need a position: numbering output lines, treating the first item differently, comparing an item with the one before it. Take it when you need it and leave it out when you do not, because a parameter you never use is a small lie about what the code depends on.
The third argument, the array itself, is rarely useful — you almost always already have a name for that array. It matters mainly because it explains a famous trap you will meet in the next lesson, where a built-in function accepts a second argument that means something completely different from an index.
You cannot break out of a forEach
A common mistake
Writing break inside a forEach callback is a syntax error. break belongs to loops, and your callback is a function — from its point of view there is no loop to break out of. The engine rejects the program before it runs, which is at least an honest failure.
The subtler mistake is reaching for return instead. That is valid, and it does something, but not what you hoped: return ends the current call of the callback, so the next element is still visited. It behaves like continue, not like break. Code that uses return in a forEach believing it stops the iteration is doing all the remaining work anyway, silently.
So if you might need to stop early, do not use forEach. Use a for...of loop with break, or use one of the methods designed to stop on their own — find, some and every all short-circuit, and they are two lessons away.
forEach returns undefined, so it never chains
Explanation
forEach hands back undefined. Not the array, not the results of your callback — nothing. It exists purely for the effects your callback has: printing, pushing onto another array, updating a counter that lives outside the loop.
That is why const doubled = numbers.forEach((n) => n * 2) leaves doubled as undefined. The callback's return values are computed and thrown away. If what you want is a new array of results, you want map, which is the next lesson, and this specific confusion is the reason map is taught immediately after.
It is also why you cannot write array.forEach(...).filter(...): there is no array on the left of that second dot, only undefined, and reading .filter from undefined throws a TypeError.
When a plain loop is still the clearer choice
Explanation
forEach is not automatically better than for...of. Prefer a plain loop when you might need to stop early, because forEach cannot; when the body is long enough that a nested function adds a layer of indentation without adding meaning; and when you are working with something that is iterable but is not an array, since forEach is an array method and a plain string does not have one.
Prefer forEach when the body is short, when you are already in the middle of a chain of array methods and want to keep the same rhythm, and when handing the job to the array genuinely reads better than describing the walk yourself.
There is one more honest reason to reach for a plain loop that has nothing to do with performance: a for...of loop is the easiest thing in JavaScript to step through in a debugger, because there is no extra function call between you and the body. When you are trying to work out why something is wrong, that matters more than elegance.
Recap
Recap
forEach runs a function once per element, passing the element, its index and the array. It returns undefined, so it cannot be chained and cannot collect results. break is a syntax error inside it and return acts like continue, so it cannot stop early — use for...of with break, or find/some/every, when that matters. It skips holes in sparse arrays. Choose it for short bodies and effects; choose a plain loop for early exits, long bodies, and easier debugging.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
Numbering a list with the index argument
The common case: a short body, and the second argument used to produce a human-friendly position. Note that the callback declares two parameters and forEach supplies both without being asked.
1const guests = ["Mo", "Ada", "Sam"];2 3guests.forEach((guest, index) => {4 console.log(index + 1 + ". " + guest);5});- line 3
Two parameters: the element first, then its index. The names are yours to choose; the order is not.
- line 4
index is 0 on the first pass, so index + 1 gives the 1, 2, 3 a human expects. The addition happens before the text is joined on, because both operands are still numbers at that point.
What it prints
1. Mo2. Ada3. Sam
return does not stop a forEach
Proof that return inside the callback behaves like continue rather than break, and that forEach itself hands back nothing at all.
1const numbers = [1, 2, 3, 4];2 3numbers.forEach((n) => {4 if (n > 2) {5 return;6 }7 console.log("kept " + n);8});9 10const result = numbers.forEach((n) => n * 10);11console.log("forEach returned: " + result);- line 5
This ends THIS call of the callback. forEach carries on and still calls it for 3 and for 4.
- line 10
The callback returns a number every time, and forEach discards every one of them.
- line 11
undefined, not [10, 20, 30, 40]. If you want the results, you want map.
What it prints
kept 1kept 2forEach returned: undefined
Elements added during the loop are not visited
A rule that only bites once, but bites hard: forEach fixes the number of elements to visit before the first call, so pushing during the loop does not extend it. The array really does grow — forEach simply stops where it planned to.
1const queue = ["a", "b"];2 3queue.forEach((item) => {4 console.log(item);5 if (item === "a") {6 queue.push("c");7 }8});9 10console.log(queue);- line 6
This really does append "c" to the array — the array is not a copy.
- line 10
The array now has three elements, but the callback only ever ran for "a" and "b". A while loop over a shrinking or growing queue is the honest tool for work that feeds itself.
What it prints
ab["a","b","c"]
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
You want to stop halfway through a forEach. What actually works?
Two lines that look like they should do the same thing. Predict both printed lines before you read the answer.
const numbers = [1, 2, 3];const fromForEach = numbers.forEach((n) => n * 2);console.log(fromForEach);console.log(numbers);
In what order does forEach pass arguments to your callback?
Write it yourself
Exercise · core · 6 tests
Build a numbered list
Write a function called numberedLines that takes an array of strings and returns a NEW array where each string is prefixed with its human position and a full stop. For example ["milk", "eggs"] becomes ["1. milk", "2. eggs"]. Use forEach and push the finished strings onto a result array. An empty input gives an empty array, and the array you were given must not change.
numberedLines(items) returns a new array of the same length, in the same order, where the element at position i is (i + 1) + ". " + items[i]. numberedLines([]) is []. The input array is untouched, and each call returns a fresh array rather than the same one.
Define numberedLines at the top level of your code — the tests call it by name.
Worth remembering
- What does array.forEach(...) return?
undefined, always. The callback's return values are discarded, so forEach cannot collect results and cannot be chained. Use map when you want the results.
- How do you stop a forEach early?
You do not. break is a syntax error inside the callback and return only ends the current call, behaving like continue. Use for...of with break, or find/some/every, which short-circuit by design.
- What three arguments do the array iteration methods pass to your callback?
The element, then its index, then the whole array — in that order. Declare only the parameters you need; the extra arguments are simply ignored.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
MDN — Array.prototype.forEach()
Check there: forEach calls the callback once for each element in ascending index order with (element, index, array), returns undefined, skips empty slots, and the range of elements is set before the first call so appended elements are not visited.
Check there: some stops as soon as the callback returns a truthy value — the short-circuit forEach lacks.
MDN — Callback function (glossary)
Check there: A callback is a function passed as an argument to be called by the receiving function.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown