Module 4 · if, switch and Loops
Every Statement, One by One
In this lesson
- Write every control-flow statement, from
ifto a labelledbreak, and say how many times its test and its body run. - Explain why a
switchcase withoutbreakruns into the next one, and use that on purpose for cases that share a body. - Stop a loop early with
break, skip the rest of a pass withcontinue, and leave two loops at once with a label.
Bob writes a switch that turns a day number into its name. For day 6 he expects one line, Saturday. His program prints three: Saturday, Sunday and not a day. No error appears, because JavaScript did exactly what he wrote. The switch section shows why, and the one-word fix.
Control flow is the order in which statements run. Lesson 01 drew the picture: if, else, the for loop and for...of. This lesson visits every statement once: its syntax, a small example, its real output and its cost. The cost is a one-line table under the example: how many times the test and the body run.
if and else
if and else
if (condition) {
runs when condition is truthy
} else {
runs when condition is falsy
}
conditionis any expression, read as truthy or falsy (Module 2). Theelsepart is optional.
const stock = 0;
if (stock > 0) {
console.log("in stock:", stock);
} else {
console.log("sold out");
}
if ("0") {
console.log("the string \"0\" is truthy");
}
sold out
the string "0" is truthy
| How often | The test | A block |
|---|---|---|
| every time the line is reached | once | exactly one of the two runs once; with no else, one or none |
The one thing to watch: a non-empty string is truthy, even "0". Module 2 has the full falsy list. So if asks "truthy or falsy?", never "is this the number 0?".
else if chains: the first true condition wins
An else if chain
if (first) {
...
} else if (second) {
...
} else {
runs when no condition was truthy
}
- The conditions are tested from the top. The first truthy one runs its block, and the rest are skipped.
const temp = 31;
if (temp >= 35) {
console.log("very hot");
} else if (temp >= 25) {
console.log("warm");
} else if (temp >= 15) {
console.log("mild");
} else {
console.log("cold");
}
if (temp >= 15) {
console.log("Bob's order says: mild");
} else if (temp >= 25) {
console.log("Bob's order says: warm");
}
warm
Bob's order says: mild
| How often | The tests | A block |
|---|---|---|
| k conditions | top down, stopping at the first truthy one: at most k (here 2) | exactly one, or none with no final else |
The one thing to watch: the order. 31 is at least 15, so Bob's chain stops at its first test and never reaches temp >= 25. With ranges, test the highest bound first, or the lowest first with <.
switch: one value against many cases
switch
switch (value) {
case A:
runs when value === A
break;
case B:
case C:
shared by B and C
break;
default:
runs when no case matched
}
valueis worked out once, then compared with each case from the top using===, so no conversion happens.breakleaves theswitch. Without it, the run carries on into the next case's lines.
Here is Bob's program, run for real.
const day = 6;
switch (day) {
case 5:
console.log("Friday");
case 6:
console.log("Saturday");
case 7:
console.log("Sunday");
default:
console.log("not a day");
}
Saturday
Sunday
not a day
A case is only a place to start. The switch finds the first case equal to day, jumps there, and runs every line below it. It stops at a break or at the closing brace. This is called fall-through. Add break; as the last line of each case, and the same program prints only Saturday.
Fall-through has one honest use: several cases that share one body. Stack the cases with nothing between them, as the months below do. A case that does have lines and falls on purpose gets a // falls through comment, so the next reader knows it is not Bob's bug.
const month = 4;
let days;
switch (month) {
case 2:
days = 28;
break;
case 4:
case 6:
case 9:
case 11:
days = 30;
break;
default:
days = 31;
}
console.log("month", month, "has", days, "days");
const plan = "pro";
switch (plan) {
case "pro":
console.log("cloud backup");
// falls through
case "basic":
console.log("email support");
break;
default:
console.log("no extras");
}
month 4 has 30 days
cloud backup
email support
| How often | The matching | The bodies |
|---|---|---|
| every time the line is reached | the value once, then cases top down until one is === (day 6: two checks) | from the match down to the first break |
The one thing to watch: === means the string "6" never matches case 6, so the default runs (Common mistake 3). The month code ignores leap years, and default is reached only after every case fails.
while: test first, then the body
while
while (condition) {
body
}
- The test runs before every pass. When it is falsy the first time, the body never runs.
let height = 16;
let bounces = 0;
while (height >= 1) {
console.log("bounce to", height);
height = height / 2;
bounces++;
}
console.log("bounces:", bounces, "height now:", height);
bounce to 16
bounce to 8
bounce to 4
bounce to 2
bounce to 1
bounces: 5 height now: 0.5
| How often | The test | The body |
|---|---|---|
| n passes | n + 1 times (here 6: the last one answers false) | n times, possibly 0 (here 5) |
A while loop suits an unknown count: you know when to stop, not how many passes it takes. The one thing to watch: the body must move the condition towards false. Delete height = height / 2; and the loop never ends. On the Playground, a run like that stops at the time limit, about 10 seconds, with "Time Limit Exceeded".
do...while: the body runs at least once
do...while
do {
body
} while (condition);
- The body runs first, then the test decides whether to go again. Note the semicolon after the closing parenthesis.
let n = 10;
while (n < 5) {
console.log("while body, n =", n);
n++;
}
do {
console.log("do body, n =", n);
n++;
} while (n < 5);
console.log("after both loops, n =", n);
do body, n = 10
after both loops, n = 11
The condition n < 5 is false from the start. The while loop asks first, so its body never runs. The do...while loop runs its body, then asks. Step through it.
Here are the two loops as flowcharts. The only difference is where the test sits.
| How often | The test | The body |
|---|---|---|
| n passes, n at least 1 | n times, once after each pass (here 1) | n times, never 0 (here 1) |
The one thing to watch: reach for do...while only when the body must run before the question makes sense. A menu shown once before the first choice, or counting the digits of 0, are the classic cases (Example 2).
for: start, test, step
for
for (start; test; step) {
body
}
for (;;) { ... } all three parts empty: runs until a break
startruns once,testbefore every pass,stepafter every pass (lesson 01).- Any of the three parts may be left empty. An empty test counts as true.
for (let i = 3; i >= 1; i--) {
console.log("countdown", i);
}
let k = 0;
for (; k < 5; ) {
k += 2;
}
console.log("k is", k);
countdown 3
countdown 2
countdown 1
k is 6
The second loop has no start and no step: k was made before it, and the body changes it. That is a while loop wearing a for loop's clothes, so write while (k < 5) instead. for (;;) is a loop with no test at all. It is not a typo: it runs until a break inside it, as the break section shows.
| How often | start, test, step | The body |
|---|---|---|
| n passes | start once, test n + 1 times, step n times (countdown: 1, 4, 3) | n times (countdown: 3) |
The one thing to watch: the counter is a let. A const counter throws TypeError: Assignment to constant variable. at the first step (lesson 01).
for...of: every value of an array or a string
for...of
for (const item of list) {
body, with item holding the next value
}
listis something with values in order: an array (Module 7) or a string (Module 3).- Each pass gets a fresh
item, soconstworks here, unlike a classic for loop's counter.
const prices = [120, 45, 300];
let total = 0;
for (const price of prices) {
total += price;
}
console.log("total:", total);
for (const ch of "hi!") {
console.log(ch);
}
total: 465
h
i
!
| How often | The test | The body |
|---|---|---|
| n values | none to write; the loop asks for the next value n + 1 times | n times (here 3, then 3 again) |
The one thing to watch: for...of gives values, not positions. When you need the position too, use a classic for loop with i. A plain object has no order of values to give, so for...of refuses it (Common mistake 4).
for...in: the keys of an object, and the array trap
for...in
for (const key in object) {
body, with key holding the next key, always a string
}
- A key is the name of one part of an object. Module 8 teaches objects; this is a preview.
const stock = { pens: 12, books: 0, bags: 5 };
for (const item in stock) {
console.log(item, stock[item]);
}
const marks = [90, 75];
for (const i in marks) {
console.log(i, typeof i, i + 1);
}
pens 12
books 0
bags 5
0 string 01
1 string 11
stock[item] reads the part whose name is in item. The second loop is the trap. On an array, for...in gives the positions as strings, "0" and "1". So i + 1 joins text and gives "01", Module 2's + rule.
| How often | The test | The body |
|---|---|---|
| k keys | none to write | k times, once per key (here 3, then 2) |
The one thing to watch: for...in is for an object's keys. Over an array, use for...of for values, or a classic for loop for positions.
break: leave the loop now
break
break; leaves the innermost loop or switch around it
const queue = [4, 9, -1, 7];
for (const ticket of queue) {
if (ticket < 0) {
console.log("bad ticket, stop");
break;
}
console.log("serve", ticket);
}
let rounds = 0;
for (;;) {
rounds++;
if (rounds === 3) {
break;
}
}
console.log("rounds:", rounds);
serve 4
serve 9
bad ticket, stop
rounds: 3
The ticket 7 is never read: the loop ended at -1. The for (;;) loop has no test, so its break is the only way out.
| How often | The test | The body |
|---|---|---|
| a break in pass p | no more tests after it | p passes, the last one cut short (tickets: 3 of 4) |
The one thing to watch: break must sit inside a loop or a switch. Anywhere else, the program does not start (Common mistake 1).
continue: skip to the next pass
continue
continue; skips the rest of this pass; the loop goes on
for (let seat = 1; seat <= 6; seat++) {
if (seat % 3 === 0) {
continue;
}
console.log("seat", seat);
}
seat 1
seat 2
seat 4
seat 5
Seats 3 and 6 are skipped. In a for loop, continue jumps to the step, so seat++ still runs and the loop moves on.
| How often | The test | The body |
|---|---|---|
| n passes | n + 1 times, as before (here 7) | starts n times, finishes only where no continue fired (here 6 and 4) |
The one thing to watch: a while loop has no step part. A continue placed before the line that changes the counter skips that line too, and the loop never ends.
Labelled break and continue Intermediate
A label
name: for (...) {
for (...) {
break name; leaves the loop called name
continue name; goes to the next pass of the loop called name
}
}
- A label is a name and a colon written just before a loop. Only
breakandcontinueuse it.
outer: for (let row = 1; row <= 9; row++) {
for (let col = 1; col <= 9; col++) {
if (row * col > 20) {
console.log("first product above 20:", row, "x", col);
break outer;
}
}
}
rows: for (let row = 1; row <= 3; row++) {
for (let col = 1; col <= 3; col++) {
if (col > row) {
continue rows;
}
console.log(row, col);
}
}
first product above 20: 3 x 7
1 1
2 1
2 2
3 1
3 2
3 3
A plain break would leave only the inner loop, and the outer one would start row 4. break outer leaves both at once. continue rows abandons the rest of a row and starts the next one, which prints a triangle.
| How often | The tests | The inner body |
|---|---|---|
| 9 by 9 grid, break outer at 3 x 7 | no test of either loop after the break | 9 + 9 + 7 = 25 times, not 81 |
The one thing to watch: a label names a loop, and it must be spelled the same at both ends (Common mistake 2). A small function with return (Module 5) often does the same job; a label fits when the loops share many variables.
Alice reads 20 pages a day. How many days does a 130-page book take, and how many pages are left for the last day?
const pages = 130;
const perDay = 20;
let read = 0;
let days = 0;
while (read < pages) {
read += perDay;
days++;
}
console.log("days:", days);
console.log("pages on the last day:", pages - (read - perDay));
days: 7
pages on the last day: 10
The count is unknown before the loop starts, so a while loop fits. After six days she has read 120 pages, and the seventh day finishes the last 10.
Run in Compilerdo...whileA number's digits are counted by cutting off the last digit until nothing is left. The number 0 still has one digit, which is why this loop tests at the end.
const numbers = [0, 7, 42, 90210];
for (const number of numbers) {
let n = number;
let digits = 0;
do {
digits++;
n = Math.floor(n / 10);
} while (n > 0);
console.log(number, "has", digits, "digit(s)");
}
0 has 1 digit(s)
7 has 1 digit(s)
42 has 2 digit(s)
90210 has 5 digit(s)
Math.floor(n / 10) drops the last digit, Module 1's rounding down. With a while loop, the test 0 > 0 would fail at once, and 0 would get 0 digits.
Amara's bus card starts with a balance. Each ride costs a fare. A ride the balance cannot cover is refused and the card waits for the next ride. When the balance reaches exactly 0, the card stops working for the day.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
let balance = nextInt();
const rides = nextInt();
let paid = 0;
for (let ride = 1; ride <= rides; ride++) {
const fare = nextInt();
if (fare > balance) {
out.push("ride " + ride + ": refused, balance " + balance);
continue;
}
balance -= fare;
paid++;
out.push("ride " + ride + ": paid " + fare + ", balance " + balance);
if (balance === 0) {
out.push("card empty after ride " + ride);
break;
}
}
out.push("rides paid: " + paid);
console.log(out.join("\n"));
ride 1: paid 30, balance 70
ride 2: paid 50, balance 20
ride 3: refused, balance 20
ride 4: paid 20, balance 0
card empty after ride 4
rides paid: 3
That output is for the input 100 5 on one line and 30 50 40 20 10 on the next. Ride 3 costs 40 with 20 left, so continue skips the payment lines. Ride 4 empties the card, so break ends the loop and ride 5 is never read.
Where this is used
- Redux. The Redux documentation writes a reducer as a
switch (action.type), onecaseper action and adefaultthat returns the state unchanged. - ESLint. Its
no-fallthroughrule, on in the recommended set, reports a case that falls into the next one. A comment matching "falls through" tells it the fall is on purpose. - TypeScript. The
noFallthroughCasesInSwitchcompiler option makes a case with code that falls into the next one an error. Empty stacked cases, like the months above, are still allowed. - Node's REPL. Typing
nodealone opens a read, evaluate, print loop: it reads a line, runs it and prints the result until you type.exit. That is a loop that runs until a sentinel, a stop value that is not part of the data.
Common mistakes
1. break outside a loop.
const stock = 0;
if (stock === 0) {
console.log("sold out");
break;
}
The program stops before any line runs: SyntaxError: Illegal break statement. An if is not a loop, so there is nothing for break to leave. Delete it, or put the rest of the work in an else. You will write it because break feels like "stop here".
2. continue to a label that does not exist.
rows: for (let r = 1; r <= 2; r++) {
for (let c = 1; c <= 2; c++) {
if (c === 2) {
continue row;
}
console.log(r, c);
}
}
The program stops before any line runs: SyntaxError: Undefined label 'row'. The label is rows, and continue row names a loop that does not exist. Spell it the same at both ends. You will make it because the two names differ by one letter.
3. A token string against a number case.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
const choice = next();
switch (choice) {
case 1:
out.push("new game");
break;
case 2:
out.push("quit");
break;
default:
out.push("unknown option");
}
console.log(out.join("\n"));
unknown option
For the input 1, there is no error and the wrong branch runs. next() gives the string "1", and "1" === 1 is false. Read with nextInt(), or write case "1":. You will make it because == would have matched, and switch never uses ==.
4. for...of over a plain object.
const week = { mon: 7500, tue: 8000 };
for (const steps of week) {
console.log(steps);
}
TypeError: week is not iterable
Iterable means "can hand out its values one by one", as an array and a string can. A plain object cannot, so the loop throws before its first pass. Use for (const day in week) and read week[day]. You will make it because for...of and for...in differ by two letters.
David marks a class test out of 100 and turns each mark into a letter. Some marks were typed wrongly, so a mark outside 0 to 100 is reported as invalid.
Input. n, then n whole-number marks.
Output. One line per mark. 90 to 100 gives A, 80 to 89 gives B, and 70 to 79 gives C. 60 to 69 gives D, and 0 to 59 gives F. A mark below 0 or above 100 gives invalid.
Constraints. 1 <= n <= 100000; -1000 <= mark <= 1000.
Sample. Input 7, then 95 89 70 65 59 101 -1, gives A, B, C, D, F, invalid and invalid on seven lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Graded as letter-grade. The hidden tests try every boundary mark, such as 89 and 90, 0 and 100, and marks just outside the range.
Bob wants his day-name switch to work this time. Days are numbered 1 (Monday) to 7 (Sunday), and the two weekend days should share one body.
Input. n, then n whole numbers.
Output. One line per number: the day's English name, a space, and weekday (1 to 5) or weekend (6 and 7); invalid for any other number. The names are Monday, Tuesday, Wednesday, Thursday, Friday, Saturday and Sunday.
Constraints. 1 <= n <= 100000; -1000 <= each number <= 1000.
Sample. Input 4, then 1 6 7 9, gives Monday weekday, Saturday weekend, Sunday weekend and invalid on four lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Graded as day-name. The hidden tests try every day from 1 to 7, and numbers such as 0, 8 and negative ones.
Zara logs the money in and out of her stall as numbers, and types 0 when the day is over. She wants to know how many entries she made and what they add up to.
Input. Whole numbers separated by spaces or line breaks. The list ends at the first 0, which is not part of it; anything after that 0 is ignored. There is always at least one 0.
Output. Two lines: how many numbers came before the first 0, and their sum.
Constraints. At most 100000 numbers before the first 0; each between -1000000 and 1000000.
Sample. Input 5 -2 7 0 4 4 gives 3 and 10 on two lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Graded as sum-until-zero. The hidden tests try a 0 as the very first number, negative numbers and numbers after the 0.
Alice builds a playlist for a bus ride of a fixed length. She plays the songs in order, but skips any song longer than 300 seconds. When the next song would take the total past the ride's length, she stops and plays nothing more.
Input. n and the ride's length L in seconds, then n song lengths in seconds.
Output. One line per song k, in order. A song longer than 300 gives skip k. Otherwise, when the total plus this song is more than L, print stop at k and no more song lines. Otherwise print play k. Last, print total, a space and the seconds played.
Constraints. 1 <= n <= 1000; 1 <= L <= 100000; 1 <= each length <= 1000.
Sample. Input 5 600, then 200 400 180 250 90, gives play 1, skip 2, play 3, stop at 4 and total 380. Song 4 would make 630, past 600.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. Use one continue and one break, and try a ride so short that the first song stops it.
Maria books cinema seats for k friends who want to sit side by side in one row. The hall is r rows of c seats, each free or taken. She wants the first block of k free seats, scanning rows from the front and seats from the left.
Input. r, c and k, then r lines of c numbers each: 0 for a free seat, 1 for a taken one.
Output. One line: the row and the first seat of the first block, both counted from 1 and separated by a space. Print no seats when there is no such block.
Constraints. 1 <= r, c <= 1000; 1 <= k <= c.
Sample. Input 3 5 3, then the rows 1 0 0 1 0, 0 1 0 0 0 and 0 0 0 0 0, gives 2 3: row 2, seats 3 to 5.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. Read seat by seat without storing the hall, and leave both loops with one labelled break at the first block.
Common doubts
Does the last case of a switch need a break?
No, the closing brace ends the run anyway. Many teams write it all the same. Then a case added below later cannot inherit a fall-through by accident.
When is
do...whilereally the right loop?When the body has to run before the test means anything. Showing a menu before the first choice, or cutting digits off a number that may be 0, are the usual cases. Everywhere else, a while loop reads more plainly.
Is
for (;;)a bug?No, it is a loop that says "forever" on purpose. It needs a
break(or areturn, Module 5) inside, or it really does run forever.while (true)means the same, and many people find it easier to read.Is a label the same as a goto?
No. JavaScript has no goto. A label can only be used by
breakandcontinue, and only to leave or restart a loop that holds them. It can never jump into the middle of other code.
Key takeaways
- An
else ifchain runs the first truthy branch only, so put the strictest test first. switchcompares with===and runs from the matching case down to abreak; stacked empty cases share a body, and a deliberate fall gets a// falls throughcomment.- A while loop tests n + 1 times for n passes and may run its body zero times;
do...whileruns its body at least once. - A for loop's parts may be empty, and
for (;;)runs until abreak;for...ofgives values,for...ingives keys as strings. breakleaves the innermost loop or switch,continueskips to the next pass, and a label lets both reach an outer loop.- Go deeper: CP and Interview Pack, nested loops, early exit and the bug gallery (Pro).
Next, lesson 03 puts these statements to work in seven full programs, from FizzBuzz to a prime sieve.
End of lesson 2
Mark it done, and your progress moves with you.
Next: Full Programs: From FizzBuzz to a Prime Sieve