JavaScript forEach Loop: A Visual Guide

Aug 14, 2026·24 min read

An array contains three order totals, and the same callback needs to run once for each one. forEach() handles that repetition, but its rules about missing elements, mutations, return values and promises decide which callbacks actually finish when you expect.

JavaScript forEach() is an array method that invokes a callback for each existing indexed element in ascending order, passes the value, index and array to that callback, and returns undefined.

What Is a JavaScript forEach Loop?

JavaScript has no foreach keyword. Array.prototype.forEach() is a method, which means you call it on an array with dot notation:

const quantities = [2, 4, 1];

quantities.forEach((quantity) => {
  console.log(quantity);
});
2
4
1

The function passed to forEach() is a callback. forEach() invokes that function for the element at index 0, then the element at index 1, and continues through the starting index range in ascending order.

One element causes one callback callindex 02index 14index 21firstthenthencallback: 2callback: 4callback: 1The callback is a function that forEach calls repeatedly.
forEach visits existing indices in ascending order and invokes the callback once at each one.

Developers commonly call this a “forEach loop” because it repeats an operation. The distinction still matters. A for or for...of construct is a loop statement whose body supports loop controls such as break and continue. A forEach() call invokes a separate function for each visited element.

That makes forEach() a natural fit when each element should cause a side effect. Logging a value, adding it to an external total, updating an existing object and changing DOM elements all fit that description.

It is not the method for every kind of repetition. If the task is to calculate and collect one new value for every old value, map() states that intention directly. Arrays covers the wider collection of array methods, while Loops: while and for explains the loop statements themselves.

The method is also generic. Its algorithm works with an object that has a length and indexed properties, although arrays are the ordinary case and the clearest place to learn its rules.

forEach Syntax and Callback Arguments

The basic call has one required argument, the callback:

array.forEach(callback);

A second form supplies the value that a traditional function receives as this:

array.forEach(callback, thisArg);

The callback itself can receive three arguments:

Three views of the same visitindex 018index 121index 219value: 21index: 1arraysame objectcallback(value, index, array)
Each callback call can receive the current value, its index, and the original array.
  • value is the value at the current index.
  • index is the current numeric index.
  • array is the object being traversed.

Most callbacks need only the value:

const filenames = ['notes.md', 'index.js', 'styles.css'];

filenames.forEach((filename) => {
  console.log(filename);
});
notes.md
index.js
styles.css

Add the index when the element’s position matters:

const filenames = ['notes.md', 'index.js', 'styles.css'];

filenames.forEach((filename, index) => {
  console.log(`${index}: ${filename}`);
});
0: notes.md
1: index.js
2: styles.css

The third argument is the same array reference on every callback invocation. That lets a callback inspect the collection around its current element:

const temperatures = [18, 21, 19];

temperatures.forEach((temperature, index, array) => {
  console.log(index, temperature, array === temperatures, array.length);
});
0 18 true 3
1 21 true 3
2 19 true 3

All three calls receive temperatures as their array argument. The reference is not a frozen snapshot, so mutations made through it affect the original array.

A normal function can receive a thisArg. Say a report callback needs a prefix stored on another object:

const reporter = {
  prefix: 'score',
};

[8, 10].forEach(function (score, index) {
  console.log(`${this.prefix} ${index}: ${score}`);
}, reporter);
score 0: 8
score 1: 10

reporter becomes this during each callback call. Without a supplied thisArg, the specification passes undefined; what a non-strict traditional function exposes can then depend on ordinary function this rules.

Arrow functions behave differently because they keep their lexical this. Passing a thisArg does not replace it:

Where does this come from?thisArg: reportertraditionalfunctionthis = reporterthisArg: ignoredsurrounding thisarrow callbacktraditional functionarrow function
thisArg affects a traditional callback, while an arrow callback keeps its surrounding this.
const reporter = { prefix: 'score' };

[8].forEach((score) => {
  console.log(reporter.prefix, score);
}, { prefix: 'ignored' });
score 8

When an arrow callback needs the surrounding object, referring to that object by name is clearer. When a normal callback is deliberately written around this, pass thisArg as the second argument.

How forEach Runs, Step by Step

Start with three shipment counts and an external total. The callback logs all three arguments, changes the total and returns a number:

const shipments = [3, 5, 2];
let total = 0;

const result = shipments.forEach((value, index, array) => {
  total += value;
  console.log(index, value, array === shipments, total);
  return value * 10;
});

console.log(result, total);
0 3 true 3
1 5 true 8
2 2 true 10
undefined 10

The complete callback trace is:

Visitvalueindexarray === shipmentsSide effect on totalCallback result
first30true0 becomes 330, discarded
second51true3 becomes 850, discarded
third22true8 becomes 1020, discarded

forEach() does not collect or otherwise use 30, 50 or 20. After the final callback, the method returns undefined.

That is the central difference from map(). A map() callback’s result becomes an element in a new array, while a forEach() callback’s result goes nowhere:

Same callback, different destinationforEachmap[4, 7, 2][4, 7, 2]callback results8 · 14 · 4callback results8 · 14 · 4results discardedreturns undefinednew array[8, 14, 4]
forEach discards callback results; map collects them into a new array.
const prices = [4, 7, 2];

const forEachResult = prices.forEach((price) => price * 2);
const mapResult = prices.map((price) => price * 2);

console.log(forEachResult);
console.log(mapResult.join(', '));
undefined
8, 14, 4

Missing indices add another rule. An empty slot is not the same thing as an element containing undefined:

const statuses = [];
statuses[0] = undefined;
statuses[2] = 'ready';

statuses.forEach((status, index) => {
  console.log(index, String(status));
});

console.log(0 in statuses, 1 in statuses, 2 in statuses);
0 undefined
2 ready
true false true

Index 0 exists, so its undefined value reaches the callback. Index 1 is missing, so forEach() skips it. Index 2 exists and runs normally.

An empty slot has no element at that index. An explicit undefined is an element whose value happens to be undefined.

Existence is checked before valueindex 0 existsvalue: undefinedindex 1 missingno valueindex exists?YESindex exists?NOcallback getsundefinedno callbackskip the hole
An existing undefined value is visited; a missing array slot is skipped.

This check for an existing index is part of the method’s defined behavior, not a truthiness test. Values such as undefined, null, 0, false and an empty string still trigger the callback when their indices exist. The ECMAScript forEach() algorithm states the steps precisely.

Practical forEach Examples

The simplest use is logging each value while inspecting a program:

const activeFiles = ['app.js', 'router.js', 'store.js'];

activeFiles.forEach((file) => {
  console.log(`loading ${file}`);
});
loading app.js
loading router.js
loading store.js

Logging is a side effect. The array remains unchanged, and no replacement array is needed.

A total also works, but the accumulator lives outside the callback because forEach() does not carry a callback result forward:

const invoiceLines = [1250, 799, 450];
let totalCents = 0;

invoiceLines.forEach((lineCents) => {
  totalCents += lineCents;
});

console.log(totalCents);
2499

The code runs in index order and adds each value to totalCents. For a calculation whose whole purpose is producing one accumulated value, another array method may communicate the result more directly, but this example exposes the mechanics without hiding the changing total.

Callbacks can also update objects already stored in an array:

const tasks = [
  { title: 'write tests', done: false },
  { title: 'update docs', done: false },
];

tasks.forEach((task) => {
  task.done = true;
});

console.log(tasks[0].done, tasks[1].done);
true true

forEach() did not mutate the array by itself. The callback changed the done property of each object, and those are the same objects held by tasks.

That distinction matters. The method controls which callbacks run; the callback controls what those calls do.

Browser code often uses forEach() for DOM side effects. This example adds a class and an accessible label to each matching button:

const buttons = document.querySelectorAll('.save-button');

buttons.forEach((button, index) => {
  button.classList.add('is-ready');
  button.setAttribute('aria-label', `Save document ${index + 1}`);
});

There is no output block because the snippet changes DOM elements rather than printing text. The visible result belongs on the page. Styles and classes covers the browser APIs used for that kind of update.

These examples share one rule: each existing item causes an observable action. That rule keeps forEach() out of code that is really trying to construct a transformed or filtered array, which also makes the intention easier to see during a coding style review.

The Rules That Cause forEach Bugs

A forEach() callback is a function body, so break and continue do not control the surrounding method call. A bare return ends only the current callback invocation:

const scores = [7, 0, 9];

scores.forEach((score) => {
  if (score === 0) return;
  console.log(score);
});

console.log('finished');
7
9
finished

That return resembles a continue in this small example because it skips the remaining statements for the current value. It cannot stop the later callback for 9.

return exits one call, not forEach709log 7return fromthis calllog 9forEach moves to the next index
return leaves the current callback, then forEach proceeds to the next index.

An exception does stop forEach() through abrupt completion. Throwing an exception merely to imitate break is normally the wrong design, though. Use some(), find(), for...of or a regular loop when stopping early is part of the task.

Mutation has two timelines. forEach() reads the traversal length before the first callback, but it reads an existing element’s value when that index is visited.

Here is one mutation sequence containing a changed value, a deletion and an append:

const stages = ['draft', 'review', 'publish'];

stages.forEach((stage, index, array) => {
  if (index === 0) {
    array[1] = 'edit';
    array.push('archive');
  }

  if (index === 1) {
    delete array[2];
  }

  console.log(index, stage, array.length);
});

console.log(2 in stages, stages[3]);
0 draft 4
1 edit 4
false archive

The mutation timeline is:

MomentStarting rangeChangeWhat forEach() does
before callbacksindices 0 through 2starting length is 3only this range can be visited
visit index 0unchangedindex 1 becomes 'edit'; index 3 is appendedlater index 1 reads 'edit'; index 3 stays outside the range
visit index 1unchangedindex 2 is deletedthe callback has already received 'edit'
check index 2unchangedindex 2 is missingthe callback is skipped
finishunchangedindex 3 still contains 'archive'the appended element is not visited

The starting length is captured. The starting values are not.

Fixed boundary, live contentsBefore the first callback0: draft1: review2: publishcaptured range: indices 0–2When later indices are checked0: draft1: edit2: missing3: archivevisitedvisitedskippednotvisited
forEach keeps the starting index range but reads each element when its turn arrives.

This rule explains why editing an unvisited value affects its later callback, deleting an unvisited element removes that callback, and appending beyond the original range adds data without adding another visit. Mutating an array during traversal can be valid, but the code now depends on timing that a reader must reconstruct.

Two other failures are more direct. Passing something that is not callable as the callback throws a TypeError, and an error thrown by a real callback stops the method before later elements are visited.

Why async and await Do Not Work as Expected

An async function returns a promise. forEach() invokes the callback but discards its return value, so it neither collects nor waits for that promise.

The event order shows the gap:

console.log('before');

['draft', 'review'].forEach(async (stage) => {
  console.log(`start ${stage}`);
  await Promise.resolve();
  console.log(`finish ${stage}`);
});

console.log('after');
before
start draft
start review
after
finish draft
finish review

Both callbacks start during the forEach() call. Each reaches await and returns a pending promise, but forEach() moves to the next index without using it. The outer code prints after before either callback resumes.

The await works inside each callback. What it does not do is delay forEach() or the statement after it. Event loop: microtasks and macrotasks explains when those promise continuations get their turn.

For sequential work, use for...of and await each operation before advancing:

async function saveInOrder(documents) {
  for (const document of documents) {
    await saveDocument(document);
  }
}

The second save starts only after the first awaited call settles. This is the right shape when ordering matters or when one operation depends on the previous one.

When the operations may begin concurrently, create one promise per element with map() and await the combined promise:

async function saveTogether(documents) {
  const promises = documents.map((document) => saveDocument(document));
  const savedDocuments = await Promise.all(promises);
  return savedDocuments;
}

map() collects the promises, and Promise.all() produces a promise for their combined completion. This is not a replacement for strict sequential work because the calls to saveDocument() begin as map() visits their elements.

Two correct async shapesfor…of: sequentialmap: concurrentawait save Astart → finishawait save Bstart → finishB starts after A finishessave Arunningsave BrunningPromise.all waitsfor bothA and B overlap in time
Sequential await prevents overlap; map with Promise.all deliberately allows it.

The contrast is small but decisive:

  • forEach(async callback) starts callbacks and ignores their returned promises.
  • for...of with await handles one operation before starting the next iteration.
  • Promise.all() with map() keeps every returned promise and waits for the group.

Async failures make the choice more than a matter of printed order. The surrounding function can await the for...of sequence or the promise returned by Promise.all(). It cannot await a promise that forEach() never returns.

When to Use forEach and When Not To

Choose the construct from the result the code needs, not from which syntax is shortest.

TaskUseReason
perform one side effect for each existing elementforEach()the callback result is not needed
create one transformed value per elementmap()callback results form a new array
keep elements that pass a testfilter()matching values form a new array
ask whether any element passessome()it can finish after a match
retrieve the first matching elementfind()it returns that element and can finish early
use break, continue or sequential awaitfor...ofthe work stays inside a loop statement
start async work concurrently and await all resultsPromise.all() with map()the promises are collected and awaited
control indices, bounds or update expressions directlyforthe loop statement exposes those controls

Use forEach() when the sentence describing the work sounds like “for each existing item, perform this action.” Use map() when it sounds like “turn every item into another value,” and use filter(), some() or find() when a test decides what result is needed.

Reach for for...of when control flow belongs inside the repetition. Reach for Promise.all() with map() when asynchronous operations can overlap and the calling code still needs one promise to await.

That choice rule scales beyond one example. JavaScript Projects for Practice: 18 Ideas With Specs gives it larger collections to work against, and JavaScript Fundamentals puts arrays, functions and control flow into one continuous path.

Frequently asked questions

What is a forEach loop in JavaScript?
Array.prototype.forEach() is an array method that calls a callback once for each existing indexed element. Developers call it a loop because it repeats work, but JavaScript has no foreach keyword or forEach loop statement.
What does forEach return in JavaScript?
forEach() always returns undefined. Return values from its callback are discarded, so use map() when the goal is to create a transformed array.
Can you break out of a JavaScript forEach loop?
Normal break and continue control do not apply to forEach() because its callback is a function, not a loop body. An exception stops the method, but some(), find(), for...of, or a regular loop usually expresses early completion more clearly.
Does forEach work with async and await?
An async callback can contain await, but forEach() does not wait for the promise that callback returns. Use for...of for sequential asynchronous work or Promise.all() with map() when the operations may run concurrently.
Does forEach skip undefined values?
forEach() visits an element whose value is undefined because that index exists. It skips an empty slot whose index is missing from a sparse array.