Learn / JavaScript fundamentals
LESSON 9 OF 84

JavaScript fundamentals Foundation

Loops

Repeat work with for, while, do...while, for...of and for...in, control a loop with break and continue, and build a number guessing game.

  • 35 min to read and try
  • You need: The lessons up to Making decisions
  • You build: A number guessing game, tested with a fixed list of guesses, that you can also play in your terminal
Test yourself

Why loops exist

Say you want to print the numbers 1 to 5. You could write five console.log lines. But what about 1 to 1000? Or one line for every task in a list whose length you do not know yet? A loop runs the same block of code again and again, as long as you need. Each run of the block is called an iteration.

The for loop

The classic for loop is best when you know how many times to repeat. Its parentheses hold three parts, separated by semicolons:

for.js
for (let i = 1; i <= 5; i++) {
  console.log(`Iteration ${i}`);
}
console.log("Loop finished");
Output of node for.js and of the browser terminal
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
Loop finished
  1. let i = 1 runs once, before the loop starts. It creates the counter.
  2. i <= 5 is checked before every iteration. When it is false, the loop stops.
  3. i++ runs after every iteration and moves the counter on. You saw ++ in the lesson on operators.

The counter does not have to go up by one. Here it counts down, and adds up a total on the way:

for-sum.js
let total = 0;
for (let n = 10; n > 0; n -= 2) {
  total += n;
  console.log(`n=${n} total=${total}`);
}
Output of node for-sum.js and of the browser terminal
n=10 total=10
n=8 total=18
n=6 total=24
n=4 total=28
n=2 total=30

while and do...while

A while loop repeats as long as its condition is truthy. Use it when you do not know in advance how many iterations you need, only when to stop:

while.js
let balance = 100;
let months = 0;

while (balance < 200) {
  balance = balance * 1.1;   // grows 10% a month
  months++;
}

console.log(`${months} months, balance ${balance.toFixed(2)}`);
Output of node while.js and of the browser terminal
8 months, balance 214.36

The condition is checked before each iteration. If it is false from the start, the body never runs. A do...while loop checks after each iteration, so its body always runs at least once:

do-while.js
let tries = 0;

while (tries > 0) {
  console.log("while body");       // never runs
}

do {
  tries++;
  console.log(`do...while body, try ${tries}`);
} while (tries < 0);
Output of node do-while.js and of the browser terminal
do...while body, try 1

You will rarely need do...while. It fits "do this, then ask whether to do it again", like asking a user for input until it is valid.

Infinite loops

If the condition never becomes false, the loop never ends and your program hangs. while (balance < 200) without the line that changes balance would run forever. Always check that something inside the loop moves it towards the end. If your program hangs in the terminal, press Ctrl+C to stop it.

for...of: one iteration per item

Most of the time you loop over a list of things. for...of gives you each item in turn, with no counter to manage. You met it briefly in the first lesson:

for-of.js
const titles = ["Buy milk", "Write report", "Call Ada"];

for (const title of titles) {
  console.log(`- ${title}`);
}

for (const letter of "abc") {
  console.log(letter.toUpperCase());
}
Output of node for-of.js and of the browser terminal
- Buy milk
- Write report
- Call Ada
A
B
C

The square brackets make an array, an ordered list; arrays come two lessons from now. for...of also works on strings, one character at a time. You can use const for the loop variable because each iteration gets a fresh one.

for...in: the keys of an object

for...in loops over the keys (the property names) of an object. task[key] reads the property whose name is stored in key:

for-in.js
const task = { title: "Buy milk", done: false, priority: 2 };

for (const key in task) {
  console.log(`${key}: ${task[key]}`);
}
Output of node for-in.js and of the browser terminal
title: Buy milk
done: false
priority: 2

Do not use for...in on arrays

On an array, for...in gives you the positions as strings ("0", "1", …), not the items. Use for...of for arrays. In the lesson on objects you will also see Object.entries, which is often clearer than for...in.

break and continue

Two keywords change the flow inside any loop:

  • break stops the whole loop immediately.
  • continue skips the rest of this iteration and goes on with the next one.
break-continue.js
const tasks = ["Buy milk", "", "Write report", "STOP", "Call Ada"];

for (const title of tasks) {
  if (title === "") {
    console.log("(skipping an empty title)");
    continue;
  }
  if (title === "STOP") {
    console.log("(stop marker found)");
    break;
  }
  console.log(`Saving "${title}"`);
}
Output of node break-continue.js and of the browser terminal
Saving "Buy milk"
(skipping an empty title)
Saving "Write report"
(stop marker found)

"Call Ada" was never reached, because break ended the loop. continue works like a guard clause from the previous lesson, but for one iteration.

Nested loops

A loop can sit inside another loop. For every iteration of the outer loop, the inner loop runs completely:

nested.js
for (let row = 1; row <= 3; row++) {
  let line = `row ${row}:`;
  for (let col = 1; col <= 4; col++) {
    line += String(row * col).padStart(4);
  }
  console.log(line);
}
Output of node nested.js and of the browser terminal
row 1:   1   2   3   4
row 2:   2   4   6   8
row 3:   3   6   9  12

That is a small multiplication table. padStart(4) pads each number with spaces to 4 characters, so the columns line up. 3 outer iterations × 4 inner iterations = 12 cells. Nested loops multiply quickly: two loops over 1000 items each make a million iterations. A break inside the inner loop only stops the inner loop.

Choosing the right loop

  • for...of: you have a list (or a string) and want each item. This is the one you will use most.
  • for: you need a counter, a fixed number of repeats, or the position of each item.
  • while: you repeat until something happens, and you cannot know in advance how many times.
  • do...while: like while, but the body must run at least once.
  • for...in: the keys of an object. Never for arrays.

In the lesson on arrays you will meet methods like map and filter, which replace many loops with one line.

Build: a number guessing game

The computer picks a secret number from 1 to 100. The player guesses; after each guess the game says "too low", "too high" or "correct". The player has 7 tries.

A game that reads the keyboard is hard to test, because a test cannot type. So split it in two: a function with the rules, which you can test with a fixed list of guesses, and a small interactive shell around it.

Step 1: the rules, tested

guess-game.js
function checkGuess(secret, input) {
  const guess = Number(input);
  if (String(input).trim() === "" || !Number.isInteger(guess)) return "not a whole number";
  if (guess < 1 || guess > 100) return "out of range (1 to 100)";
  if (guess < secret) return "too low";
  if (guess > secret) return "too high";
  return "correct";
}

function play(secret, guesses, maxTries = 7) {
  let tries = 0;
  for (const input of guesses) {
    const result = checkGuess(secret, input);
    if (result !== "too low" && result !== "too high" && result !== "correct") {
      console.log(`  ${input}: ${result}, try again`);
      continue;                     // a bad guess does not cost a try
    }
    tries++;
    console.log(`  try ${tries}: ${input} is ${result}`);
    if (result === "correct") return `won in ${tries} tries`;
    if (tries === maxTries) return `lost, the number was ${secret}`;
  }
  return "gave up";
}

console.log("Game 1:", play(42, ["50", "25", "abc", "37", "43", "40", "42"]));
console.log("Game 2:", play(99, ["10", "20", "30", "40", "50", "60", "70", "80"]));
console.log("Game 3:", play(7, ["7"]));
Output of node guess-game.js and of the browser terminal
  try 1: 50 is too high
  try 2: 25 is too low
  abc: not a whole number, try again
  try 3: 37 is too low
  try 4: 43 is too high
  try 5: 40 is too low
  try 6: 42 is correct
Game 1: won in 6 tries
  try 1: 10 is too low
  try 2: 20 is too low
  try 3: 30 is too low
  try 4: 40 is too low
  try 5: 50 is too low
  try 6: 60 is too low
  try 7: 70 is too low
Game 2: lost, the number was 99
  try 1: 7 is correct
Game 3: won in 1 tries

Look at how the output shows every rule working:

  • Game 1: "abc" was rejected without using up a try (that is the continue), and the loop stopped at the correct guess (that is the return).
  • Game 2: after 7 tries the game ended, even though an eighth guess, "80", was in the list.
  • Game 3 prints "1 tries". That is a small bug the test found. Exercise 1 below fixes it.

Because the secret is a parameter, the test chooses it. In the real game it comes from Math.random().

Step 2: play it in your terminal

To read what the player types, Node.js has a built-in module, node:readline/promises. This needs Node.js, so it does not run in the browser terminal. await pauses until the player presses Enter; the lesson on asynchronous JavaScript explains how it works. For now, copy the lines as they are.

guess.jsNode.js only
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";

function checkGuess(secret, input) {
  const guess = Number(input);
  if (String(input).trim() === "" || !Number.isInteger(guess)) return "not a whole number";
  if (guess < 1 || guess > 100) return "out of range (1 to 100)";
  if (guess < secret) return "too low";
  if (guess > secret) return "too high";
  return "correct";
}

const secret = Math.floor(Math.random() * 100) + 1;
const maxTries = 7;
const rl = createInterface({ input: stdin, output: stdout });

console.log("I picked a number from 1 to 100. You have 7 tries.");
let tries = 0;
while (tries < maxTries) {
  const input = await rl.question(`Guess ${tries + 1}: `);
  const result = checkGuess(secret, input);
  if (result !== "too low" && result !== "too high" && result !== "correct") {
    console.log(`${result}, try again`);
    continue;
  }
  tries++;
  if (result === "correct") {
    console.log(`Correct! You needed ${tries} ${tries === 1 ? "try" : "tries"}.`);
    break;
  }
  console.log(result);
  if (tries === maxTries) console.log(`Out of tries. The number was ${secret}.`);
}
rl.close();

Save it as guess.js in the hello-zudo folder you made in Set up your computer. That folder's package.json has "type": "module", which import and top-level await need. Run it and play. Your secret number and guesses will be different; one real game looked like this:

Terminal on your computer
node guess.js
I picked a number from 1 to 100. You have 7 tries.
Guess 1: fifty
not a whole number, try again
Guess 1: 50
too low
Guess 2: 75
too high
Guess 3: 62
too high
Guess 4: 56
too high
Guess 5: 53
too high
Guess 6: 51
too low
Guess 7: 52
Correct! You needed 7 tries.

A clever player always guesses the middle of the numbers that are left. That halves the range each time, so 7 tries are always enough for 100 numbers: 2 to the power of 7 is 128.

Practice

TRY IT YOURSELF

Fix “1 tries”

Change play so that it prints won in 1 try for one guess and won in 3 tries for three.

Show a solution

Only the winning line changes. Choose the word with a ternary:

guess-plural.js
function wonMessage(tries) {
  return `won in ${tries} ${tries === 1 ? "try" : "tries"}`;
}

console.log(wonMessage(1));
console.log(wonMessage(3));
Output of node guess-plural.js and of the browser terminal
won in 1 try
won in 3 tries

In play, replace return `won in ${tries} tries`; with return wonMessage(tries);.

TRY IT YOURSELF

FizzBuzz

Print the numbers 1 to 15, but print Fizz for multiples of 3, Buzz for multiples of 5 and FizzBuzz for multiples of both. Put everything on one line, separated by spaces.

Show a solution
fizzbuzz.js
let line = "";
for (let i = 1; i <= 15; i++) {
  if (i % 15 === 0) line += "FizzBuzz ";
  else if (i % 3 === 0) line += "Fizz ";
  else if (i % 5 === 0) line += "Buzz ";
  else line += `${i} `;
}
console.log(line.trim());
Output of node fizzbuzz.js and of the browser terminal
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz

The check for 15 must come first. Otherwise 15 would match "multiple of 3" and stop there.

TRY IT YOURSELF

First long title

Given ["Buy milk", "Call Ada", "Write the quarterly report", "Pay rent"], find and print the first title longer than 10 characters and its position, then stop looking.

Show a solution
first-long.js
const titles = ["Buy milk", "Call Ada", "Write the quarterly report", "Pay rent"];

for (let i = 0; i < titles.length; i++) {
  if (titles[i].length > 10) {
    console.log(`Found at position ${i}: ${titles[i]}`);
    break;
  }
}
Output of node first-long.js and of the browser terminal
Found at position 2: Write the quarterly report

You need the position, so a counting for loop fits. titles[i] reads the item at position i, and titles.length is how many items there are.

Recap

  • A loop repeats a block. Each run is an iteration.
  • for has a counter; while repeats until a condition fails; do...while always runs once.
  • for...of gives each item of a list or string; for...in gives the keys of an object.
  • break stops the loop; continue skips to the next iteration.
  • Make sure every loop moves towards its end, or it runs forever.
  • Put logic in a function you can test with fixed input, and keep the keyboard part thin.

Test yourself

Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.