Loops
Mental model: Every loop answers two questions: what do I get each time round, and what makes it stop?
Level: beginner · about 13 minutes
const stock = ['pen', 'pad', 'ink'];
for (let i = 0; i < stock.length; i++) {
console.log(i, stock[i]);
}
// → 0 'pen'
// → 1 'pad'
// → 2 'ink'The classic three-part for: start, keep going while, and step.
The header has three slots separated by semicolons: an initialiser that runs once, a condition checked before every pass, and an update that runs after every pass. Any of the three can be left empty, which is how for (;;) becomes an infinite loop.
Use the counting for when you need the index, when you step by something other than one, or when you walk backwards. If you only want the values, there is a better loop below.
while and do...while
while: test, then maybe run
let queue = ['a', 'b'];
while (queue.length > 0) {
console.log(queue.shift());
}
// an empty queue runs the body zero times
do...while: run, then test
let attempt = 0;
do {
attempt += 1;
} while (attempt < 0);
console.log(attempt); // 1, the body ran onceReach for while when the number of passes is not known up front (draining a queue, reading until a sentinel). do...while is rare and exists for the case where the body must run at least once, such as prompting for input before you can validate it.
for...of gives you values
const stock = ['pen', 'pad', 'ink'];
for (const item of stock) {
console.log(item); // → 'pen', 'pad', 'ink'
}
// Need the index as well? Ask for both.
for (const [i, item] of stock.entries()) {
console.log(i, item); // → 0 'pen', 1 'pad', 2 'ink'
}
for...in gives you keys, as strings
const settings = { theme: 'dark', size: 14 };
for (const key in settings) {
console.log(key, '=', settings[key]); // → 'theme = dark', 'size = 14'
}
const scores = [10, 20];
for (const i in scores) {
console.log(i, typeof i); // → '0 string', '1 string'
}
console.log(0 in scores, '0' in scores); // → true trueLook closely at the types in the second loop.
const scores = [10, 20];
let total = 0;
for (const i in scores) {
total += i;
}
console.log(total);for...in yields the keys '0' and '1' as strings. total starts as the number 0, so 0 + '0' coerces to the string '00', then '00' + '1' gives '001'. The bug is silent: no error, just a string where you expected a sum.
forEach is a method, not a loop
const nums = [1, 2, 3, 4];
nums.forEach((n) => {
if (n === 3) return; // returns from THIS callback only, like `continue`
console.log(n); // → 1, 2, 4
});
// There is no way to stop early. `break` is a syntax error here.
// Need to stop? Use for...of with break, or some/every/find.
| Loop | You get | Can break? | Use it for |
|---|---|---|---|
for (let i…) | an index you control | yes | steps, reverse order, index maths |
while | nothing; you manage state | yes | unknown number of passes |
do...while | nothing; body runs first | yes | at least one pass required |
for...of | each value | yes | the default choice for arrays and iterables |
for...in | each key, as a string | yes | plain objects only |
forEach | value, index, array | no | a side effect on every element, no early exit |
Off by one, in both directions
const items = ['a', 'b', 'c'];
for (let i = 0; i <= items.length; i++) {
console.log(i, items[i]); // last pass logs → 3 undefined
}
// Valid indexes are 0 to length - 1, so the condition is `<`, not `<=`.
// Counting from 1? Then `i <= length` is right and you index with `items[i - 1]`.
init ──► condition ──false──► done
│ true
▼
body
│
▼
update ───────┐
▲ │
└───────────┘ back to the condition
Try it yourself
Five loops, one array
const letters = ['a', 'b', 'c'];
console.log('--- for ---');
for (let i = 0; i < letters.length; i++) console.log(i, letters[i]);
console.log('--- while ---');
let n = 0;
while (n < letters.length) { console.log(letters[n]); n++; }
console.log('--- for...of ---');
for (const letter of letters) console.log(letter);
console.log('--- for...of with index ---');
for (const [i, letter] of letters.entries()) console.log(i, letter);
console.log('--- forEach ---');
letters.forEach((letter, i) => console.log(i, letter));
Add a sixth version that walks the array backwards. Then change < to <= in the counting loop and read the last line of output.
A loop with a safety valve
let value = 27;
const steps = [];
let guard = 0;
while (value !== 1) {
steps.push(value);
value = value % 2 === 0 ? value / 2 : value * 3 + 1;
guard += 1;
if (guard > 1000) { console.log('safety valve tripped'); break; }
}
console.log('steps taken:', steps.length);
console.log('first few:', steps.slice(0, 6));
Remove the guard check and think about what would happen. Then fix the real bug so the loop terminates on its own.
Exercises
Count the vowels
Write countVowels(text) which returns how many of a, e, i, o, u the string contains, ignoring case. Walk the characters with for...of, because you want the values and not the indexes.
Chunk an array
Write chunk(items, size) which splits an array into groups of size. The final group may be shorter. Return an empty array when size is less than 1, and never modify the input.
Check yourself
- What does this log?
- 'string' twice —
for...initerates property keys, and object keys are strings. That is why index maths inside afor...inover an array goes wrong, and whyfor...of(or a countingfor) is the right tool. - You need to stop iterating as soon as you find a match. Which cannot do that?
Array.prototype.forEach—forEachcalls your function once per element and ignores what it returns, so there is nothing to break out of.returninside the callback only ends that one call, behaving likecontinue. Usefor...of,find,someoreverywhen you need to stop.- When is
do...whilethe right choice overwhile? - When the body must execute at least once before the condition can be evaluated — The condition is checked after the body, so the body always runs at least once. That fits cases like asking for input before you can validate it. Everywhere else,
whilestates the intent more clearly.
Common mistakes
- Using
for...inon an array, then doing arithmetic on keys that are strings. - Writing
i <= items.length, which reads one past the end and gives youundefined. - Pushing to the same array you are looping over, so the condition never becomes false.
Takeaways
for...ofyields values,for...inyields keys as strings, a countingforgives you the index.forEachcannot break;returninside it behaves likecontinue.- Valid indexes stop at
length - 1, so the condition is<and not<=. - Every loop needs a line that moves the condition towards false. If you cannot name it, you have an infinite loop.