Loops & Iteration
The 8 looping mechanisms
for · while · do...while · for...in · for...of · labeled · break · continue
Quick selection guide:
| Loop | Use for | Condition checked | Runs ≥ once |
|---|---|---|---|
for | known count / index access | before each pass | no |
while | condition-driven, unknown count | before each pass | no |
do...while | must run at least once | after each pass | yes |
for...of | values of any iterable | per element | no |
for...in | enumerable keys of an object | per property | no |
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);