Skip to main content

Loops & Iteration

The 8 looping mechanisms

for · while · do...while · for...in · for...of · labeled · break · continue

Quick selection guide:

LoopUse forCondition checkedRuns ≥ once
forknown count / index accessbefore each passno
whilecondition-driven, unknown countbefore each passno
do...whilemust run at least onceafter each passyes
for...ofvalues of any iterableper elementno
for...inenumerable keys of an objectper propertyno

Advanced Patterns & Edge Cases

for...in walks the prototype chain

for...in visits all enumerable string-keyed properties, including inherited ones — not just own properties. Guard with Object.hasOwn (or hasOwnProperty):

Object.prototype.injected = "oops";
const obj = { a: 1, b: 2 };

for (const key in obj) {
console.log(key); // a, b, injected ← inherited leaks in
}

for (const key in obj) {
if (Object.hasOwn(obj, key)) {
console.log(key); // a, b
}
}

Never use for...in on arrays

It iterates keys as strings and includes any custom/expando properties. Order is also not guaranteed across engines.

const arr = [10, 20, 30];
arr.customProp = "hello";

for (const i in arr) console.log(i);
// "0", "1", "2", "customProp" ← strings + custom prop

for (const v of arr) console.log(v);
// 10, 20, 30 ← values only, in order

Rule of thumb: for...of for values, for...in for object keys, classic for when you need the index.

for...of works on anything iterable — not just arrays

Strings, Map, Set, arguments, NodeLists, generators all implement the iterator protocol. Plain objects do not — that's a TypeError.

for (const [k, v] of Object.entries({ a: 1, b: 2 })) {
console.log(k, v); // a 1 / b 2
}

for (const ch of "héllo") console.log(ch); // iterates by code point, not UTF-16 unit

const obj = { a: 1 };
for (const x of obj) {} // ✗ TypeError: obj is not iterable

continue in a for loop still runs the afterthought

continue skips the rest of the body but does not skip the update expression — so i++ still fires. In a while, continue jumps straight back to the condition (easy way to write an accidental infinite loop if the counter lives in the body).

// while: continue BEFORE the increment = infinite loop
let i = 0;
while (i < 5) {
if (i === 2) continue; // i never advances past 2
i++;
}

Labels break/continue outer loops

A bare break/continue only affects the innermost loop. Use a label to target an outer one — the cleanest alternative to flag variables in nested loops.

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

outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (grid[i][j] === target) {
console.log(`found ${target} at [${i}][${j}]`); // [1][1]
break outer; // exits BOTH loops
}
}
}

continue label restarts the labeled loop, not the inner one it sits in.

let vs var in loop closures (classic interview trap)

let creates a fresh binding per iteration, so closures capture distinct values. var has one function-scoped binding shared by all iterations.

const fns = [];
for (let i = 0; i < 3; i++) fns.push(() => i);
console.log(fns.map((f) => f())); // [0, 1, 2] ✓

for (var j = 0; j < 3; j++) fns.push(() => j);
// all closures see the final j === 3

await belongs in for...of, not forEach

for...of (and for await...of) respects await and runs sequentially. Array.prototype.forEach ignores the returned promise — the loop doesn't wait, and rejections go unhandled.

// ✓ sequential, awaited
for (const url of urls) {
const res = await fetch(url);
}

// ✓ stream of async iterables
for await (const chunk of stream) {
process(chunk);
}

// ✗ forEach does NOT await — runs all at once, errors swallowed
urls.forEach(async (url) => { await fetch(url); });

do...while runs the body before testing

Condition is evaluated after the first pass, so the body always executes at least once — even when the condition is false from the start.

let n = 10;
do {
console.log(n); // logs 10 once
} while (n < 5);