Tutorials

JavaScript ES6 Loops

Key Takeaways

  • Use for...of for iterable values and for...in for enumerable string property keys, including inherited keys.
  • A for loop groups initialization, a condition, and an update. A while loop checks its condition first; do...while runs its body at least once.
  • For an object's own enumerable keys, use Object.keys() or guard a for...in loop with Object.hasOwn().
  • Use break to exit a loop and continue to skip the rest of an iteration. Neither controls an enclosing loop from inside a forEach() callback.
  • Use forEach() for synchronous callback-style side effects and map() to produce a transformed array. forEach() doesn't wait for async callbacks.

JavaScript loops repeat work while a condition holds or while a collection supplies more items. Start with for...of when you need array values, a counted for when you need index control, and for...in when you need enumerable object keys.

The important distinction is what you want to visit: values, property names, or a sequence controlled by a condition. These constructs complement the branching statements covered in JavaScript ES6 Control Flow.

Choosing Among JavaScript Loops

Choose a construct by its behavior, not by whether someone calls it a “definite” or “indefinite” loop. A for loop doesn't guarantee a known iteration count, and an iterable doesn't have to contain a fixed number of values.

Your intentChooseWhy
Control a counter or indexforInitialization, condition, and update stay together.
Repeat while a condition holdswhileChecks the condition before each iteration.
Run once, then decide whether to repeatdo...whileChecks the condition after the body.
Visit iterable valuesfor...ofSupports ordinary loop control.
Visit enumerable property keysfor...inIncludes inherited enumerable string keys.
Perform synchronous callback-style side effectsforEach()Calls a function for each present array element.
Create a transformed arraymap()Builds an array from callback results.

The MDN loops and iteration guide documents the loop statements; array methods provide a separate callback-based approach.

Run each code example separately in a modern browser console or JavaScript runtime.

for, while, and do...while Loops

A JavaScript for Loop Groups the Moving Parts

for (let i = 0; i < 5; i++) {
  console.log(i);
}

// logs 0, 1, 2, 3, 4

The initializer runs once. Before each iteration, JavaScript checks i < 5; after the body, i++ advances the counter. This example runs five times, but a different condition could depend on changing runtime state or never become false.

A while Loop Checks Before Running

let x = 0;

while (x < 5) {
  console.log(x);
  x++;
}

// logs 0, 1, 2, 3, 4

A while loop runs its body only while its condition is truthy. If the condition is false on the first check, the body doesn't run. Make sure something can change the condition or exit the loop.

A do...while Loop Runs Before Checking

let i = 0;

do {
  console.log(i);
  i++;
} while (i < 5);

// logs 0, 1, 2, 3, 4

The condition comes after the body, so do...while always enters its body at least once. If this example started with i = 5, it would still log 5 once. Keep the trailing semicolon after the condition.

for...of vs for...in: Values Versus Keys

Use for...of for Array Values

const array = [1, 2, 3, 4];

for (const x of array) {
  console.log(x);
}

// logs 1, 2, 3, 4

With the normal array iterator, for...of yields values in index order. The variable receives the element, not its property name. A const declaration works because each iteration gets a new binding; see JavaScript let, const, and var for the underlying variable rules.

Now add a property that isn't an array index:

const colors = ["red", "blue"];
colors.label = "palette";

for (const value of colors) {
  console.log(value);
}
// red
// blue

for (const key in colors) {
  console.log(key, typeof key);
}
// 0 string
// 1 string
// label string

The array iterator doesn't enumerate the added label property. The for...in loop does, and even the index keys are strings. This semantic difference—not a blanket performance claim—is why for...of is the better default for array values.

Don't replace the old ordering myth with a new one: modern for...in enumeration has defined ordering rules. It's still property enumeration, not array-value iteration.

Use for...in for Enumerable Object Keys

const obj = { a: 1, b: 2 };

for (const key in obj) {
  console.log(key);
}

// logs 'a', 'b'

An enumerable property is one whose property settings allow it to participate in enumeration. for...in visits enumerable string-keyed properties, including inherited ones; it ignores symbol-keyed properties.

An own property belongs directly to the object rather than coming from its prototype, the object it inherits from. When you only want own properties, add a guard:

const defaults = { inherited: "default" };
const settings = Object.create(defaults);
settings.own = "custom";

for (const key in settings) {
  console.log(key);
}
// own
// inherited

for (const key in settings) {
  if (Object.hasOwn(settings, key)) {
    console.log(key, settings[key]);
  }
}
// own custom

Object.hasOwn(object, key) checks ownership without depending on an object's own hasOwnProperty method. It also works with objects whose prototype is null.

For ordinary own-key traversal, Object.keys() is often simpler: it returns an array of own enumerable string keys. When you need both keys and values, Object.entries() returns their pairs:

const obj = { a: 1, b: 2 };

for (const key of Object.keys(obj)) {
  console.log(key);
}
// a
// b

for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}
// a 1
// b 2

Iterable Values Aren't Just Arrays

An iterable supplies values through the iterable protocol: a standard agreement between a value and the code consuming it. Its [Symbol.iterator]() method returns an iterator, whose next() method supplies results containing a value and a completion flag named done.

for...of handles that machinery for you. Strings, maps, and sets are iterable too:

for (const letter of "cat") {
  console.log(letter);
}
// c, a, t

for (const number of new Set([1, 1, 2])) {
  console.log(number);
}
// 1, 2

const scores = new Map([["Ada", 10], ["Lin", 20]]);
for (const [name, score] of scores) {
  console.log(name, score);
}
// Ada 10
// Lin 20

A map's default iterator supplies [key, value] pairs; the bracketed declaration unpacks each pair. The iteration protocols reference explains how custom iterables participate.

Array-Like Doesn't Mean Iterable

An array-like object can have indexed properties and a length without implementing iteration. Passing the following object directly to for...of would throw a TypeError; Array.from() can convert it to an array first:

const arrayLike = { 0: "a", 1: "b", length: 2 };

console.log(typeof arrayLike[Symbol.iterator]); // undefined

for (const value of Array.from(arrayLike)) {
  console.log(value);
}
// a
// b

A plain object such as { a: 1 } isn't iterable by default either. Use Object.keys() or Object.entries() when your intent is to traverse its properties.

Controlling Loops with break and continue

Use break to Exit Early

let array = [1, 2, 3];

for (let x of array) {
  if (x === 2) {
    break;
  }
  console.log(x);
}

// logs 1

An unlabeled break exits the innermost enclosing loop or switch. Here, execution leaves the loop before logging 2.

Use continue to Skip the Rest of an Iteration

let array = [1, 2, 3];

for (let x of array) {
  if (x === 2) {
    continue;
  }
  console.log(x);
}

// logs 1, 3

An unlabeled continue skips the rest of the current iteration of the innermost enclosing loop. In a counted for, it proceeds to the update expression; in while and do...while, it proceeds to the condition.

Nested Loops and Labels

For nested loops, break outer; can exit an enclosing statement labeled outer:, while continue outer; can advance an enclosing loop with that label. Labels don't let either statement jump across a function boundary. Prefer simpler control flow unless the label makes the nested algorithm clearer.

forEach vs for...of: Understand the Callback Boundary

forEach Calls a Function; It Isn't a Loop Statement

Array.prototype.forEach() calls your callback for each present array element in ascending index order:

const array = [1, 2, 3, 4];

array.forEach((x) => {
  console.log(x);
});

// logs 1, 2, 3, 4

A callback is a function passed to another function to be called later in its execution. Inside that callback, return ends only that function call—not the overall traversal:

const array = [1, 2, 3];

array.forEach((x) => {
  if (x === 2) {
    return;
  }
  console.log(x);
});

// logs 1, 3

You can't use break or continue to control forEach() itself. Its callback return values are discarded, and the method returns undefined. Choose for...of when you need early exit or explicit iteration control.

Sparse Arrays Behave Differently

A sparse array contains missing elements, called holes. forEach() skips holes; normal array for...of iteration visits those positions and yields undefined when no indexed property exists there.

const sparse = [1, , 3];

sparse.forEach((value) => console.log(value));
// 1, 3

for (const value of sparse) {
  console.log(value);
}
// 1, undefined, 3

An explicit undefined element isn't a hole: forEach() visits it. Don't assume switching iteration styles preserves sparse-array behavior.

Use map When You Want a New Array

const numbers = [1, 2, 3, 4];
const doubled = numbers.map((number) => number * 2);

console.log(doubled); // [2, 4, 6, 8]

map() creates a new array from callback results. Use it for transformation rather than calling it only for side effects and discarding its result. For more collection operations, see JavaScript array methods.

forEach Doesn't Await Async Callbacks

Writing await array.forEach(async (value) => { ... }) doesn't wait for the callback promises: you're awaiting the method's undefined return value. For sequential asynchronous work, put await inside a for...of loop instead.

async function run() {
  const numbers = [1, 2, 3, 4];
  const doubled = [];

  for (const number of numbers) {
    // A resolved promise stands in for an asynchronous operation.
    const result = await Promise.resolve(number * 2);
    doubled.push(result);
  }

  console.log(doubled); // [2, 4, 6, 8]
}

run().catch((error) => {
  console.error("Processing failed:", error);
});

Each iteration waits at await before moving to the next number. The loop doesn't automatically await anything—you explicitly request the wait in its body. See the JavaScript async/await example for more on promise-based control flow.

Common Mistakes

  • Choosing by an assumed speed ranking. Choose the correct semantics first. Measure your actual workload before changing a loop for performance.
  • Forgetting to advance a condition. In a while loop, check that a continue can't bypass the update that allows termination.
  • Treating for...in keys as array values. Its loop variable is a string property name, not the element stored at that property.
  • Changing a collection without planning for it. Avoid adding or removing elements during traversal unless you've deliberately designed and tested that behavior.

FAQ

What is the difference between for...of and for...in in JavaScript?

for...of consumes values supplied by an iterable. for...in visits enumerable string property keys, including inherited keys, so it isn't the usual choice for traversing array values.

How do you loop through an array in JavaScript?

Use for...of to read array values with support for break and continue. Use a counted for loop when you need explicit index control, or map() when you want a transformed array.

Can you use break or continue in forEach()?

You can't use break or continue to control forEach() from its callback. A return statement exits only the current callback invocation; use for...of when you need loop-level control.

Does for...of work with plain objects?

Plain objects aren't iterable by default. Use for...of with Object.keys(object) for own enumerable string keys or Object.entries(object) for own enumerable string-keyed key-value pairs.

Does forEach() work with async functions?

forEach() can call an async function, but it doesn't wait for the returned promises. Awaiting forEach() doesn't await its callbacks; use for...of with await in the body for sequential asynchronous work.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me