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 once

Reach 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 true

Look 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.
LoopYou getCan break?Use it for
for (let i…)an index you controlyessteps, reverse order, index maths
whilenothing; you manage stateyesunknown number of passes
do...whilenothing; body runs firstyesat least one pass required
for...ofeach valueyesthe default choice for arrays and iterables
for...ineach key, as a stringyesplain objects only
forEachvalue, index, arraynoa 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...in iterates property keys, and object keys are strings. That is why index maths inside a for...in over an array goes wrong, and why for...of (or a counting for) is the right tool.
You need to stop iterating as soon as you find a match. Which cannot do that?
Array.prototype.forEach — forEach calls your function once per element and ignores what it returns, so there is nothing to break out of. return inside the callback only ends that one call, behaving like continue. Use for...of, find, some or every when you need to stop.
When is do...while the right choice over while?
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, while states the intent more clearly.

Common mistakes

  • Using for...in on an array, then doing arithmetic on keys that are strings.
  • Writing i <= items.length, which reads one past the end and gives you undefined.
  • Pushing to the same array you are looping over, so the condition never becomes false.

Takeaways

  • for...of yields values, for...in yields keys as strings, a counting for gives you the index.
  • forEach cannot break; return inside it behaves like continue.
  • 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.