JavaScript Loops (for)

6 minute read

The for loop is JavaScript’s oldest and most flexible iteration construct. It runs a block repeatedly, with explicit init / test / update steps. Modern JavaScript adds for…of (values) and for…in (keys), each with its own niche. This chapter is the practical guide for picking the right one and avoiding the classic off-by-one bugs.

Anatomy of a for Loop

for (let i = 0; i < 5; i++) {
  console.log(i);
}
// Output:
// 0
// 1
// 2
// 3
// 4

Three semicolon-separated parts:

for (initialization ; condition ; afterEach) { body }
       │              │           │
       │              │           └── runs after each iteration (i++)
       │              └────────────── checked BEFORE each iteration
       └───────────────────────────── runs once before the loop starts

All three parts are optional. for (;;) { ... } is an infinite loop until you break.

Iterating an Array With Index

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

for (let i = 0; i < fruits.length; i++) {
  console.log(`${i}: ${fruits[i]}`);
}
// Output:
// 0: apple
// 1: banana
// 2: cherry

Tip: Cache fruits.length only if your array is huge or modified inside the loop. Modern engines optimise the common case automatically — readability wins.

Reverse, Step, Stop Early

// Backwards
for (let i = arr.length - 1; i >= 0; i--) { /* … */ }

// Step by 2
for (let i = 0; i < 10; i += 2) {
  console.log(i);   // 0, 2, 4, 6, 8
}

// break — exit immediately
for (const n of nums) {
  if (n < 0) break;
  console.log(n);
}

// continue — skip to next iteration
for (const n of nums) {
  if (n % 2 !== 0) continue;
  console.log(n);   // only evens
}

for…of — Values

The modern default for iterating over iterables (arrays, strings, Set, Map, generators, NodeLists, …):

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

for (const fruit of fruits) {
  console.log(fruit);
}
// Output:
// apple
// banana
// cherry

Get the index too with entries():

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

for…of works on anything iterable:

for (const ch of "abc")            console.log(ch);   // a, b, c
for (const v of new Set([1, 2]))   console.log(v);    // 1, 2
for (const [k, v] of new Map([["a", 1]])) console.log(k, v);
for (const node of document.querySelectorAll(".x")) /* … */ ;

// Numeric range via generator (more in chapter 49)
function* range(start, stop, step = 1) {
  for (let i = start; i < stop; i += step) yield i;
}
for (const i of range(0, 5)) console.log(i);

for…in — Keys (Use Sparingly)

Iterates over an object’s enumerable string keys, including inherited ones from the prototype chain:

const user = { name: "Ada", age: 37 };

for (const key in user) {
  console.log(key, user[key]);
}
// Output:
// name Ada
// age 37

Warning: Don’t use for…in on arrays. It iterates string indices and any enumerable properties — including ones added to Array.prototype by libraries.

const arr = [10, 20];
arr.foo = "extra";

for (const i in arr) {
  console.log(i);   // "0", "1", "foo"  ← strings, plus the extra prop!
}

For objects, prefer Object.keys / Object.entries:

for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}

Decision Table

You have Use
Need the index, or step ≠ 1 classic for
Iterating values of an array / string / Set / iterator for…of
Need both index AND value for…of arr.entries()
Iterating own keys of an object for…of Object.keys/entries(obj)
Iterating a fixed number of times for (let i = 0; i < n; i++)
Iterating with await for each value for…of (works) — never forEach
Need to break out early classic for or for…of (works) — never forEach

forEach vs for…of

  forEach for…of
Can break ❌ ✅
Can continue ✅ (return) ✅
Awaits async work ❌ — kicks off all in parallel ✅ — sequential
Index passed as 2nd arg use .entries()
Style functional imperative
// ❌ This does NOT wait — fires all promises in parallel, function returns immediately
arr.forEach(async item => {
  await save(item);
});

// ✅ Sequential
for (const item of arr) {
  await save(item);
}

// ✅ Parallel (intentional)
await Promise.all(arr.map(item => save(item)));

Closures and var — The Classic Bug

Pre-ES2015 code used var, which is function-scoped. It causes a famous puzzle:

const callbacks = [];

for (var i = 0; i < 3; i++) {
  callbacks.push(() => console.log(i));
}

callbacks.forEach(cb => cb());
// Output:
// 3
// 3
// 3

All three closures captured the same i, which by the end of the loop is 3. The fix: use let, which creates a fresh binding per iteration:

const callbacks = [];

for (let i = 0; i < 3; i++) {
  callbacks.push(() => console.log(i));
}

callbacks.forEach(cb => cb());
// Output:
// 0
// 1
// 2

Tip: This is the single biggest reason to never use var. Always let or const.

Nested Loops and Labels

const grid = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

for (let r = 0; r < grid.length; r++) {
  for (let c = 0; c < grid[r].length; c++) {
    console.log(grid[r][c]);
  }
}

To break out of an outer loop, use a label:

outer:
for (const row of grid) {
  for (const cell of row) {
    if (cell === 5) {
      console.log("found!");
      break outer;       // exits BOTH loops
    }
  }
}

Labels work with continue too — but they’re rare in modern code. Refactor into a function and return instead when possible.

Common Mistakes

// ❌ Mutating array length while iterating
for (let i = 0; i < arr.length; i++) {
  if (arr[i] < 0) arr.splice(i, 1);   // shifts indices — skips elements
}

// ✅ Iterate backwards, or use filter
arr = arr.filter(x => x >= 0);

// ❌ Off-by-one with <=
for (let i = 0; i <= arr.length; i++) { /* runs ONE too many */ }

// ❌ for…in on array
for (const i in arr) { /* strings, not numbers; includes inherited */ }

// ❌ Awaiting forEach
arr.forEach(async x => await fetch(x));   // does NOT wait

// ❌ Modifying counter inside body in confusing ways
for (let i = 0; i < n; i++) {
  if (cond) i += 5;     // jumps the counter — read once, scratch your head
}

A Realistic Example — Pagination Walker

async function fetchAllPages(baseUrl) {
  const results = [];
  for (let page = 1; ; page++) {
    const res = await fetch(`${baseUrl}?page=${page}`);
    const data = await res.json();

    if (data.items.length === 0) break;   // empty page → done
    results.push(...data.items);

    if (!data.hasMore) break;
    if (page >= 100) {
      console.warn("Pagination guard tripped at 100 pages");
      break;
    }
  }
  return results;
}

A classic for with no third clause and an explicit break — the iteration count isn’t known up front, so this is a better fit than for…of.

Best Practices Checklist

  • ✅ Use for…of as your default for arrays, strings, Set, Map, iterators
  • ✅ Use classic for when you need the index, custom step, or no fixed iterable
  • ✅ Use Object.entries(obj) (with for…of) for objects
  • ✅ Use let (not var) so closures inside loops capture per-iteration bindings
  • ✅ Use break/continue/return over labels when possible
  • ❌ Don’t use for…in on arrays
  • ❌ Don’t await inside forEach — use for…of or Promise.all
  • ❌ Don’t mutate the array you’re iterating with classic for (or iterate backwards)

What’s Next?

You’ve covered the counting kind of loop. Next, while and do…while — when you don’t know the iteration count up front, these are often the cleaner fit.

← Previous: JS Switch Next: JS While Loops →

Updated: