Contents

Chapter 8

Loops: Doing It Again Until It's Done

while, for, for...of, and the loop you'll write by accident.

So far every line you write runs once. If you want something ten times you type it ten times, which you already know is not how this is supposed to work.

while

while repeats a block for as long as its condition holds. Same parentheses and braces as if, and it re-checks the condition every time round:

let count = 1;

while (count <= 3) {
  console.log(`while ${count}`);
  count++;
}
while 1
while 2
while 3

Three things have to be true for that to work, and every loop you write needs all three:

  1. A starting point. count is 1 before the loop begins.
  2. A condition that will eventually be false. count <= 3.
  3. Something inside that moves toward it. count++.

Miss the third and the loop runs forever. That’s not a hypothetical.

for

When you’re counting, all three parts belong together, and for puts them on one line:

for (let i = 1; i <= 3; i++) {
  console.log(`for ${i}`);
}
for 1
for 2
for 3

Three pieces separated by semicolons: start, keep going while, and do after each round. Exactly the same three things the while needed, just gathered where you can see them all at once, which is why a missing i++ is much harder to write by accident here.

i is the traditional name for a loop counter. It’s one of the few places a one-letter name is genuinely fine.

for…of

Most of the time you don’t care about the counting, you just want each item in turn. That’s for...of:

for (const letter of "Ada") {
  console.log(letter);
}
A
d
a

No counter, no condition, no chance of an off-by-one. It works on strings now and on arrays in chapter 10, and it’s the loop you’ll reach for most often once you have both.

Note const letter, not let. Each time round is a fresh letter, so nothing is being reassigned and const is honest.

Stopping early, and skipping one

break leaves the loop immediately:

for (let i = 1; i <= 5; i++) {
  if (i === 3) break;
  console.log(`break-demo ${i}`);
}
break-demo 1
break-demo 2

continue skips the rest of this round and starts the next:

for (let i = 1; i <= 5; i++) {
  if (i % 2 === 0) continue;
  console.log(`continue-demo ${i}`);
}
continue-demo 1
continue-demo 3
continue-demo 5

continue is how you filter without nesting. The alternative is wrapping the whole body in an if, which pushes everything one level deeper for no gain.

Keeping something between rounds

A loop that only prints is the simple case. More often you want an answer at the end, which means a variable that lives outside the loop and gets updated inside it:

let total = 0;

for (const price of [3, 5, 2]) {
  total += price;
}

console.log(total);
10

total has to be declared before the loop, because a variable declared inside would be brand new every round and thrown away at the end of it. That’s the whole pattern: start with an answer that’s true for nothing at all, then improve it once per item.

Zero is the right starting point for a sum. For other jobs it’s a different starting value, but the shape doesn’t change:

let longest = "";

for (const word of ["hi", "hello", "hey"]) {
  if (word.length > longest.length) longest = word;
}

console.log(longest);
hello

Counting is the same idea with ++, and "how many a's are in this word" is three lines you can now write without thinking about it. Chapter 14 has tools that do these in one line, and they’ll make a lot more sense for having written the loop first.

Paying off chapter 5

Chapter 5 mentioned a phantom empty string at the end of split input, and said it would matter once loops arrived. It has arrived:

const lines = "a\nb\n".split("\n");
console.log(lines);

for (const line of lines) {
  console.log(`line [${line}]`);
}
[ 'a', 'b', '' ]
line [a]
line [b]
line []

Two lines of input, three times round the loop. The trailing newline from the last answer leaves an empty string behind, and asking for lines[0] and lines[1] by hand hid that completely. A loop can’t hide it, because a loop takes everything.

The fix is one line, and it’s why continue was worth introducing:

for (const line of lines) {
  if (line === "") continue;
  console.log(`line [${line}]`);
}
line [a]
line [b]

Exercise 1 · Count the lines

Write a program that reads input, splits it, and prints how many lines it got, both with and without skipping empty ones. Feed it three lines and watch one version confidently report four.

This is the difference between a number that’s right and a number that looks right, and it’s worth meeting on a program where you know the answer.

Check yourself

1. A while loop prints the same line forever. What is almost certainly wrong?

Not quite. Possible, but a backwards condition usually means the loop runs zero times rather than forever.

Yes. The condition is re-checked every round, so if nothing it depends on ever changes, the answer never changes either. Ctrl-C stops it.

Not quite. break is for leaving early. An ordinary loop ends on its own when its condition goes false, provided something moves it there.

2. for (const i in ["a", "b"]) console.log(i); What prints?

Not quite. That is for...of. for...in walks keys rather than values, and the two are easy to mistype for each other.

Yes. for...in gives you the positions, and gives them as strings rather than numbers. Use for...of when you want the items.

Not quite. It runs happily on an array. That is exactly why it is a trap rather than an error.

3. You split three lines of typed input and loop over the result. How many times does the loop run?

Not quite. That is what indexing lines[0] to lines[2] by hand would suggest, which is why the loop is where this finally shows up.

Yes. Pressing return after the last answer produces a trailing newline, and splitting on newlines leaves an empty string after it. Skip it with continue.

Not quite. It does not. Print the split result and you can see the empty string sitting at the end.

Project

The number guessing game

Roughly 45 minutes

A secret number, and guesses that get told higher or lower until one is right. This is the first program in the book that behaves like a game.

printf '50\n25\n42\n' | node guess.js
50 is too high.
25 is too low.
42 is right! Took 3 guesses.

The shape of it:

  1. A const for the secret number at the top.
  2. Read the input and .split("\n") it, exactly as chapter 5 did.
  3. for...of over the guesses, skipping empty lines with continue.
  4. Count the attempts as you go.
  5. if / else if / else for too low, too high, correct.
  6. break when they get it, so later guesses aren’t judged.
  7. After the loop, say something if they never got it.

Rules: one read, one loop, and changing only the secret at the top should still work correctly.

Test the ending you’ll forget. Feed it guesses that never win:

printf '1\n2\n' | node guess.js
1 is too low.
2 is too low.
Ran out of guesses.

A loop that finishes normally and a loop that hits break are two different endings, and only one of them means they won. Tracking that takes a boolean you set before the break, which is chapter 3 and chapter 7 doing quiet work together.

Stretch: make the secret random with Math.floor(Math.random() * 100) + 1. The game gets genuinely unwinnable-by-printf, which is a fair trade for it being a real game, and it’s a good reminder that a program you can’t test the same way twice is harder to trust.