JavaScript Loops Explained: for, while, for-of, for-in — When to Use Which

Link copied
JavaScript Loops Explained: for, while, for-of, for-in — When to Use Which

JavaScript Loops Explained: for, while, for-of, for-in — When to Use Which

JS Tutorial Module 1: Foundations Lesson 1.6

JavaScript has five loop constructs: classic for, while, do...while, for...of, and for...in. They overlap. They behave differently in subtle ways. And in 2026, most loops you write should not be loops at all — they should be array methods like map, filter, and reduce.

This lesson covers every loop, explains the differences, and shows when reaching for an explicit loop is the right call vs. when an array method is cleaner.

Why looping at all? #

Loops do three jobs in JavaScript:

  1. Iterate — visit every element of a collection.
  2. Repeat — run a block until a condition stops being true.
  3. Side-effect work — DOM updates, logging, mutating shared state on each pass.

For #1 and #2, modern JavaScript often gives you a cleaner alternative (array methods, recursion, async iteration). For #3, explicit loops are usually still the right tool.

Keep that in mind as we go through the constructs.

The classic for loop #

The oldest and most flexible.

for (let i = 0; i < items.length; i++) {
  console.log(items[i]);
}

Three parts inside the parens:

  1. Initializer — runs once before the loop starts (let i = 0).
  2. Condition — checked before each iteration. Loop runs while truthy.
  3. Update — runs after each iteration (i++).

All three are optional. for (;;) {} is an infinite loop — perfectly legal.

Use classic for when you need:

  • The index, not just the value
  • A non-linear stride (i += 2, i--, i = i * 2)
  • To break early before reaching the end
  • Maximum performance on hot paths (it's the fastest loop on every engine)

while and do...while #

Both repeat while a condition is true. The difference is when the condition is checked.

// `while` — checks first, may run zero times
while (queue.length > 0) {
  process(queue.shift());
}

// `do...while` — runs once, then checks
do {
  const answer = prompt('Continue?');
  // ...
} while (answer === 'yes');

while is for "loop until this condition stops being true." do...while is for "do this at least once, then loop until done."

do...while is rare in modern code. Most situations that look like they need it are clearer with while (true) + an explicit break:

while (true) {
  const next = readNext();
  if (!next) break;
  process(next);
}

for...of — the modern iteration loop #

The most useful loop in modern JavaScript. Iterates values of any iterable.

const items = ['a', 'b', 'c'];

for (const item of items) {
  console.log(item); // 'a', 'b', 'c'
}

Works on:

  • Arrays, strings, typed arrays
  • Map, Set
  • DOM NodeList collections
  • Generators
  • Anything that implements the iterable protocol (we cover that in Module 3)

When you need the index too, pair it with entries():

for (const [i, item] of items.entries()) {
  console.log(i, item);
}

for...of is the right default for "do something to each element." Readable, works on any iterable, and the loop variable is block-scoped per iteration (so closures work as expected — see Lesson 1.2).

for...in — the dangerous one #

Iterates keys of an object. Sounds useful. Has three traps.

const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key, obj[key]); // 'a' 1, 'b' 2, 'c' 3
}

The traps:

  1. Inherited properties are included. If the object has any prototype with enumerable properties, those show up too. (Object.create(parent) for example.)
  2. Order is not guaranteed. Modern engines mostly preserve insertion order for string keys, but the spec doesn't strictly require it for all cases.
  3. It's wrong for arrays. for...in on an array iterates indices as strings ('0', '1', …) and also picks up any custom properties you may have added. Always use for...of or array methods for arrays.

Safer modern equivalent for iterating an object's own keys:

for (const key of Object.keys(obj)) {
  console.log(key, obj[key]);
}

// or with both keys and values
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}

Use for...in only when you specifically need to walk the prototype chain — which is rare.

Comparison table #

Loop Iterates Best for
for (let i = 0; ...) Any range you define Index access, custom strides, max performance
while (cond) {} Until condition fails Unknown-count loops (queue processing, polling)
do { } while (cond) At least once, then until cond fails Rare — prefer while (true) + break
for (const x of iter) Values of an iterable Default for "visit each element"
for (const k in obj) Enumerable keys (incl. inherited) Almost never — use Object.keys(obj)

Loop control: break and continue #

  • break — exit the loop immediately.
  • continue — skip to the next iteration.
for (const item of items) {
  if (item.skip) continue;
  if (item.stop) break;
  process(item);
}

Both work in every loop construct.

With nested loops, labeled breaks let you exit an outer loop:

outer: for (const row of grid) {
  for (const cell of row) {
    if (cell.isExit) break outer; // breaks the for-of-row, not the inner
  }
}

Labels are rare in idiomatic code — usually a sign you should extract the inner work into a function and return.

When not to write a loop #

For anything that produces a new array, a number from an array, or a filtered subset — use array methods.

// Loop version
const doubled = [];
for (const n of nums) doubled.push(n * 2);

// Array-method version — clearer intent
const doubled = nums.map(n => n * 2);
// Loop
let total = 0;
for (const n of nums) total += n;

// Reduce
const total = nums.reduce((sum, n) => sum + n, 0);
// Loop
const adults = [];
for (const user of users) if (user.age >= 18) adults.push(user);

// Filter
const adults = users.filter(user => user.age >= 18);

The array-method versions:

  • Express what they do (the verb is in the method name) instead of how
  • Don't need an external mutable variable
  • Don't need an index counter
  • Compose: nums.filter(n => n > 0).map(n => n * 2).reduce(...)

Use loops when:

  • You need to break early (array methods always visit every element)
  • The work is heavily side-effecting (DOM updates, logging)
  • Performance on a hot path matters and benchmarking proves the loop is faster
  • You need the index in a specific way for...of + entries() doesn't fit

We cover the full array-method catalog in Module 3 (Lesson 3.2 — Array methods cheatsheet).

A common bug: modifying the array during iteration #

Any construct that uses indices is fragile if the array changes mid-loop.

const nums = [1, 2, 3, 4, 5];
for (let i = 0; i < nums.length; i++) {
  if (nums[i] % 2 === 0) {
    nums.splice(i, 1); // BUG — shifts indices
  }
}
console.log(nums); // [1, 3, 5] — but only by accident; this misses items
// if the deletion pattern is denser

The fix: iterate backwards, or build a new array with filter.

const nums = [1, 2, 3, 4, 5].filter(n => n % 2 !== 0);

Never mutate a collection while iterating it with index-based loops. for...of on arrays has the same risk if you mutate. Build new collections instead — fewer bugs, easier to test.

Async iteration: for await...of #

Introduced in ES2018. Iterates async iterables — like streams or paginated APIs.

async function readAllChunks(stream) {
  for await (const chunk of stream) {
    console.log(chunk);
  }
}

Only works inside async functions. We cover async iteration in Module 5.

Performance: loops vs methods #

For most code, the difference doesn't matter. V8 has spent years optimizing both. Pick whichever reads better.

If you've profiled and you know a specific loop is a bottleneck, a hand-rolled classic for is the fastest option on every modern engine. Array methods have to invoke a callback for each element, which is slightly slower at the margin.

But don't optimize prematurely. "Read better, profile if slow" is the rule.

A summary checklist #

When writing a new loop, ask yourself in order:

  1. Am I producing a new collection? → use map, filter, flatMap
  2. Am I reducing to a single value? → use reduce
  3. Am I checking if something matches? → use some, every, find, findIndex
  4. Do I need to visit every element with potential side effects? → for...of
  5. Do I need the index, a custom stride, or early break? → classic for
  6. Am I waiting on a condition, unknown count? → while
  7. Am I iterating an object's own keys? → for (const k of Object.keys(obj))

That decision tree covers ~99% of cases.

What's next #

Lesson 1.7 covers basic functions — declarations, parameters, return values, and the building blocks Module 2 then takes much deeper (this, closures, hoisting, scope). With variables, types, operators, conditionals, loops, and functions covered, you'll have everything needed to write working JavaScript programs — and to read almost any modern codebase.

Try it yourself #

The for...in vs for...of distinction is one of the most common stumbling blocks. Predict the output:

YouPredict the output:
const arr = ['a', 'b', 'c'];
for (const x in arr) console.log(x);
for (const x of arr) console.log(x);
Claude · used js_sandboxThe first loop logs 0, 1, 2 — string indices, not values.
The second loop logs a, b, c — the actual values.

For arrays, almost always reach for for...of (or a method like forEach). for...in on arrays is a footgun — it returns string keys and picks up any custom properties on the array object.

One keyword difference, completely different output. The rule of thumb that keeps you out of trouble: of for values, in for keys, and prefer Object.keys(obj) when you mean "this object's own keys" anyway.

Up next in JavaScript

More from this topic

View all JavaScript articles →

Enjoyed this article?

Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email address will not be published. Required fields are marked *