JavaScript Loops (for)
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…ofas your default for arrays, strings,Set,Map, iterators - ✅ Use classic
forwhen you need the index, custom step, or no fixed iterable - ✅ Use
Object.entries(obj)(withfor…of) for objects - ✅ Use
let(notvar) so closures inside loops capture per-iteration bindings - ✅ Use
break/continue/returnover labels when possible - ❌ Don’t use
for…inon arrays - ❌ Don’t
awaitinsideforEach— usefor…oforPromise.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.