Learn JavaScript

Lesson 2 of 9 · if, switch and Loops

Module 4 · if, switch and Loops

Every Statement, One by One

FreeReading

In this lesson

  • Write every control-flow statement, from if to a labelled break, and say how many times its test and its body run.
  • Explain why a switch case without break runs 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 with continue, 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
}
  • condition is any expression, read as truthy or falsy (Module 2). The else part 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 oftenThe testA block
every time the line is reachedonceexactly 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 oftenThe testsA block
k conditionstop 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
}
  • value is worked out once, then compared with each case from the top using ===, so no conversion happens.
  • break leaves the switch. 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: without break, the run falls from case 6 into every case below it day is 6: where the run starts, and where it stops No break: falls through A break in every case case 5: skipped, 6 === 5 is false case 6: match, prints Saturday case 7: falls in, prints Sunday default: falls in, prints not a day after the switch 3 lines printed case 5: skipped, 6 === 5 is false case 6: prints Saturday, break case 7: never runs default: never runs after the switch 1 line printed

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 oftenThe matchingThe bodies
every time the line is reachedthe 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 oftenThe testThe body
n passesn + 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.

One pass: the body prints do body, n = 10 and makes n 11. Then the test 11 < 5 answers false, and the loop ends after exactly one pass.

PassPrintedn after the bodyTest n < 5
1do body, n = 101111 < 5 is false: stop

Here are the two loops as flowcharts. The only difference is where the test sits.

while tests before the body; do...while tests after it while: test first do...while: body first before the loop test? true body again false after the loop The body runs 0 or more times. before the loop body test? true false after the loop The body runs 1 or more times.
How oftenThe testThe body
n passes, n at least 1n 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
  • start runs once, test before every pass, step after 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 oftenstart, test, stepThe body
n passesstart 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
}
  • list is something with values in order: an array (Module 7) or a string (Module 3).
  • Each pass gets a fresh item, so const works 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 oftenThe testThe body
n valuesnone to write; the loop asks for the next value n + 1 timesn 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 oftenThe testThe body
k keysnone to writek 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 oftenThe testThe body
a break in pass pno more tests after itp 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 oftenThe testThe body
n passesn + 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 break and continue use 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 oftenThe testsThe inner body
9 by 9 grid, break outer at 3 x 7no test of either loop after the break9 + 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.

Example 1: Alice's reading plan

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 Compiler
Example 2: counting digits with do...while

A 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.

Run in Compiler
Example 3: Amara's bus card

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.

Run in Compiler

Where this is used

  • Redux. The Redux documentation writes a reducer as a switch (action.type), one case per action and a default that returns the state unchanged.
  • ESLint. Its no-fallthrough rule, 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 noFallthroughCasesInSwitch compiler 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 node alone 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.

Brain teaser

A switch sits inside a for loop, and one of its cases ends with break.

for (let i = 1; i <= 3; i++) {
  switch (i) {
    case 2:
      console.log("two, so break");
      break;
    default:
      console.log("pass", i);
  }
}
console.log("after the loop");

Does that break leave the loop, or only the switch? Write down every line you expect, then run it.

Read the break syntax card again: which statement is the innermost one around that break?

Exercise 1Easy

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.

Run in Compiler
Exercise 2Easy

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.

Run in Compiler
Exercise 3Easy

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.

Run in Compiler
Exercise 4Medium

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.

Run in Compiler
Exercise 5Hard

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.

Run in Compiler

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...while really 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 a return, 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 break and continue, 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 if chain runs the first truthy branch only, so put the strictest test first.
  • switch compares with === and runs from the matching case down to a break; stacked empty cases share a body, and a deliberate fall gets a // falls through comment.
  • A while loop tests n + 1 times for n passes and may run its body zero times; do...while runs its body at least once.
  • A for loop's parts may be empty, and for (;;) runs until a break; for...of gives values, for...in gives keys as strings.
  • break leaves the innermost loop or switch, continue skips 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