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.
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 (let i = 1; i <= 5; i++) {
console.log(`Iteration ${i}`);
}
console.log("Loop finished");
node for.js and of the browser terminalIteration 1 Iteration 2 Iteration 3 Iteration 4 Iteration 5 Loop finished
let i = 1runs once, before the loop starts. It creates the counter.i <= 5is checked before every iteration. When it is false, the loop stops.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:
let total = 0;
for (let n = 10; n > 0; n -= 2) {
total += n;
console.log(`n=${n} total=${total}`);
}
node for-sum.js and of the browser terminaln=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:
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)}`);
node while.js and of the browser terminal8 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:
let tries = 0;
while (tries > 0) {
console.log("while body"); // never runs
}
do {
tries++;
console.log(`do...while body, try ${tries}`);
} while (tries < 0);
node do-while.js and of the browser terminaldo...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:
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());
}
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:
const task = { title: "Buy milk", done: false, priority: 2 };
for (const key in task) {
console.log(`${key}: ${task[key]}`);
}
node for-in.js and of the browser terminaltitle: 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:
breakstops the whole loop immediately.continueskips the rest of this iteration and goes on with the next one.
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}"`);
}
node break-continue.js and of the browser terminalSaving "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:
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);
}
node nested.js and of the browser terminalrow 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: likewhile, 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
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"]));
node guess-game.js and of the browser terminaltry 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 thecontinue), and the loop stopped at the correct guess (that is thereturn). - 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.
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:
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:
function wonMessage(tries) {
return `won in ${tries} ${tries === 1 ? "try" : "tries"}`;
}
console.log(wonMessage(1));
console.log(wonMessage(3));
node guess-plural.js and of the browser terminalwon 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
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());
node fizzbuzz.js and of the browser terminal1 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
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;
}
}
node first-long.js and of the browser terminalFound 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.
forhas a counter;whilerepeats until a condition fails;do...whilealways runs once.for...ofgives each item of a list or string;for...ingives the keys of an object.breakstops the loop;continueskips 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.