Skip to editor content
learningjavascript.orglesson 4 of 25

Control Flow

Control flow is how you make your programs smart. Instead of just running code line by line, you can make decisions (if/else) and repeat actions (loops). This is where your code becomes truly powerful!

What You'll Learn

By the end of this lesson you will finish three working functions under check: one that answers a FizzBuzz-style question with an if/else if chain, one that counts matches by looping to a limit, and one that picks a fare from three numeric bands. Each fails loudly until it is right, so you will know when you are done.

You arrive able to write a function that takes parameters and returns a value (03-functions); you leave able to make that function decide — to take different paths for different inputs, and to repeat work a controlled number of times.

This lesson is not marked required for the final project, but nothing later works without it. Every one of the Task Manager's behaviours in 15-capstone-project is a decision or a loop: is this title valid, is this task pending or completed, how many are left, render every one of them.

If Statements

If statements let your code make decisions:

let temperature = 75;

if (temperature > 80) {
  console.log("It's hot outside!");
}

if (temperature <= 80) {
  console.log("The weather is nice!");
}

If/Else Statements

Use else to handle the alternative case:

let age = 20;

if (age >= 18) {
  console.log("You are an adult");
} else {
  console.log("You are a minor");
}

// Another example
let score = 85;

if (score >= 90) {
  console.log("Grade: A");
} else if (score >= 80) {
  console.log("Grade: B");
} else if (score >= 70) {
  console.log("Grade: C");
} else if (score >= 60) {
  console.log("Grade: D");
} else {
  console.log("Grade: F");
}

Predict

Trace this by hand before running it. Note that BOTH conditions are true for n = 12. What does it log?

let n = 12;

if (n > 5) {
console.log("big");
} else if (n > 10) {
console.log("bigger");
} else {
console.log("small");
}

Only the first matching branch runs. Because n > 5 is true, the chain logs "big" and skips every later else if — even though n > 10 is also true for 12. This is why the order of an if/else-if chain matters: put the more specific (narrower) conditions first, or the broad one will always win.

Putting the rule to work

That rule — first match wins, so order the branches from narrow to broad — is what pins the block below. Reassemble it.

One piece of syntax first, because the block uses it: when a branch's body is a single statement, you may drop the braces and write it on one line, as if (days > 30) return "overdue";. It means exactly what the braced form means — only the one statement immediately after the condition belongs to the branch. It is common in short chains like this one, where four braced blocks would bury the logic.

Arrange the code

Reassemble the body of tier(days), which labels a task by how long it has been waiting: more than 30 days is overdue, more than 7 is late, anything still positive is due, and 0 or less is today. Unlike the earlier blocks, EVERY arrangement of these four lines runs without an error — only one of them is correct. Why must they run in this order and no other?

  1. if (days > 0) return "due";
  2. if (days > 7) return "late";
  3. return "today";
  4. if (days > 30) return "overdue";

Why that order? Narrow first, broad last. The constraint is semantic rather than syntactic — every arrangement compiles and runs, so nothing but reasoning about the ranges will tell you which one is right.

Comparison Operators

These operators are used in conditions:

let x = 10;
let y = 20;

console.log("x === y:", x === y);  // Equal to (strict)
console.log("x !== y:", x !== y);  // Not equal to
console.log("x < y:", x < y);      // Less than
console.log("x > y:", x > y);      // Greater than
console.log("x <= y:", x <= y);    // Less than or equal
console.log("x >= y:", x >= y);    // Greater than or equal

Recall

Without scrolling up: back in 02-variables-and-types you saw that a comparison like 5 > 3 produces a value. What TYPE of value does an expression like x < y evaluate to, and why does that matter for an if condition?

Logical Operators

Combine multiple conditions:

let age = 25;
let hasLicense = true;

// AND operator (&&) - both must be true
if (age >= 18 && hasLicense) {
  console.log("You can drive!");
}

// OR operator (||) - at least one must be true
let isWeekend = true;
let isHoliday = false;

if (isWeekend || isHoliday) {
  console.log("You can relax!");
}

// NOT operator (!) - reverses the condition
let isSleeping = false;

if (!isSleeping) {
  console.log("You are awake!");
}

Ternary Operator

A shorthand for simple if/else statements:

// Syntax: condition ? valueIfTrue : valueIfFalse

let age = 20;
let status = age >= 18 ? "adult" : "minor";
console.log("Status:", status);

// Another example
let score = 85;
let result = score >= 60 ? "Pass" : "Fail";
console.log("Result:", result);

// You can use it directly
console.log("Can vote:", age >= 18 ? "Yes" : "No");

For Loops

Repeat code a specific number of times:

// Count from 1 to 5
for (let i = 1; i <= 5; i++) {
  console.log("Count:", i);
}

// Countdown from 5 to 1
for (let i = 5; i >= 1; i--) {
  console.log("Countdown:", i);
}

// Count by 2s
for (let i = 0; i <= 10; i += 2) {
  console.log("Even number:", i);
}

Debug

This loop should add up 1 + 2 + 3 + 4 + 5 and log 'Sum: 15'. Instead it crashes on the very first iteration with a TypeError before printing anything. Predict what goes wrong, then fix it so it logs 'Sum: 15'.

let sum = 0;

for (const i = 1; i <= 5; i++) {
sum += i;
}

console.log("Sum:", sum);

Expected output:

Sum: 15

While Loops

Repeat code while a condition is true:

// Count to 5 with while loop
let count = 1;
while (count <= 5) {
  console.log("While count:", count);
  count++;
}

// Find first power of 2 greater than 100
let power = 1;
while (power <= 100) {
  power = power * 2;
}
console.log("First power of 2 > 100:", power);

Do-While Loops

Like while loops, but always run at least once:

let num = 1;

do {
  console.log("Number:", num);
  num++;
} while (num <= 3);

// This runs once even though condition is false
let x = 10;
do {
  console.log("This prints once, x =", x);
} while (x < 5);

Break and Continue

Control loop execution:

// Break - exit the loop early
console.log("Using break:");
for (let i = 1; i <= 10; i++) {
  if (i === 5) {
    console.log("Breaking at", i);
    break;
  }
  console.log(i);
}

// Continue - skip to next iteration
console.log("\nUsing continue:");
for (let i = 1; i <= 5; i++) {
  if (i === 3) {
    console.log("Skipping", i);
    continue;
  }
  console.log(i);
}

Practical Examples

Let's combine what we've learned:

// Find all even numbers from 1 to 10
console.log("Even numbers from 1 to 10:");
for (let i = 1; i <= 10; i++) {
  if (i % 2 === 0) {
    console.log(i);
  }
}

// FizzBuzz (classic programming challenge)
console.log("\nFizzBuzz:");
for (let i = 1; i <= 15; i++) {
  if (i % 3 === 0 && i % 5 === 0) {
    console.log("FizzBuzz");
  } else if (i % 3 === 0) {
    console.log("Fizz");
  } else if (i % 5 === 0) {
    console.log("Buzz");
  } else {
    console.log(i);
  }
}

// Calculate factorial
let n = 5;
let factorial = 1;
for (let i = 1; i <= n; i++) {
  factorial *= i;
}
console.log("\nFactorial of", n, "is", factorial);

Try It Yourself

Reading about conditions and loops is not the same as building with them. This is a build task: a small program that reports its own pass/fail. You are given three empty functions to finish, each one a small decision or counting problem. Run it and it fails immediately, telling you which function is still missing. Implement each one until every check passes and it prints All checks passed.

The three functions reuse exactly what this lesson taught: an if/else if chain that returns the first matching result, the modulo operator to test divisibility, and a counting for loop with a running total. The starter already has the data, the stubs, and the checks — you write only the logic inside each function. Nothing above spells out all three answers, so you will have to assemble them yourself.

Build

Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide what that function is missing, then implement the three functions until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

const assert = require('assert');

// The data: fixed inputs to check against. Do NOT change these values.
const limit = 15;
const childAge = 8;
const adultAge = 40;
const seniorAge = 70;

// TODO 1: return a word for one FizzBuzz number.
//   Divisible by 3 AND 5 -> "FizzBuzz"; by 3 only -> "Fizz";
//   by 5 only -> "Buzz"; otherwise the number as a string.
//   fizzbuzzWord(15) -> "FizzBuzz", fizzbuzzWord(9) -> "Fizz", fizzbuzzWord(7) -> "7"
function fizzbuzzWord(n) {
// your code here
}

// TODO 2: count how many integers from 1 to limit are divisible by factor.
//   countDivisibleBy(15, 3) -> 5
function countDivisibleBy(limit, factor) {
// your code here
}

// TODO 3: return a ticket fare for an age.
//   Under 13 -> 5; 65 or older -> 8; everyone else -> 12.
//   fareFor(8) -> 5, fareFor(40) -> 12, fareFor(70) -> 8
function fareFor(age) {
// your code here
}

// --- Build checks: these must all pass. Do not edit below this line. ---
assert.strictEqual(
fizzbuzzWord(15),
"FizzBuzz",
"fizzbuzzWord(n) should return 'FizzBuzz' when n is divisible by both 3 and 5",
);
assert.strictEqual(
fizzbuzzWord(9),
"Fizz",
"fizzbuzzWord(n) should return 'Fizz' when n is divisible by 3 only",
);
assert.strictEqual(
fizzbuzzWord(10),
"Buzz",
"fizzbuzzWord(n) should return 'Buzz' when n is divisible by 5 only",
);
assert.strictEqual(
fizzbuzzWord(7),
"7",
"fizzbuzzWord(n) should return the number as a string when it is divisible by neither 3 nor 5",
);
assert.strictEqual(
countDivisibleBy(limit, 3),
5,
"countDivisibleBy(limit, factor) should count how many integers from 1 to limit divide evenly by factor",
);
assert.strictEqual(
fareFor(childAge),
5,
"fareFor(age) should charge 5 for anyone under 13",
);
assert.strictEqual(
fareFor(adultAge),
12,
"fareFor(age) should charge 12 for a standard adult",
);
assert.strictEqual(
fareFor(seniorAge),
8,
"fareFor(age) should charge 8 for anyone 65 or older",
);

console.log("All checks passed.");
console.log("fizzbuzzWord(15):", fizzbuzzWord(15));
console.log("countDivisibleBy(15, 3):", countDivisibleBy(limit, 3));
console.log("fareFor(70):", fareFor(seniorAge));

Expected output:

All checks passed.
fizzbuzzWord(15): FizzBuzz
countDivisibleBy(15, 3): 5
fareFor(70): 8

Once it passes, try two variations and predict each before running:

  1. Reorder the FizzBuzz chain. In fizzbuzzWord, move the divisible-by-3 test (n % 3 === 0 → "Fizz") above the both-divisible test. Predict what fizzbuzzWord(15) returns before running. 15 is divisible by 3 and by 5, but the chain returns on the first match — so it now answers "Fizz" and never reaches "FizzBuzz", and the first check fails. This is the order-matters point in your own hands.
  2. Off-by-one in the count loop. In countDivisibleBy, change the loop bound from i <= limit to i < limit. Decide what countDivisibleBy(15, 3) returns before running. Stopping at 14 drops the multiple 15 itself, so the count comes back 4 instead of 5 and the check fails — the classic inclusive-vs-exclusive loop boundary.

Taking It Somewhere New

Transfer

You have used the rule that an if/else-if chain runs only its FIRST matching branch and skips the rest. New setting: a switch statement, which JavaScript also evaluates top to bottom against its cases — but where each case runs on until it hits a break. Apply the invariant: what does this print, and what is the same and what is different about the stopping rule?

function accessLevel(role) {
switch (role) {
  case "admin":
    console.log("can delete");
  case "editor":
    console.log("can edit");
    break;
  case "viewer":
    console.log("can read");
    break;
}
}

accessLevel("admin");

Key Takeaways

  • Use if/else statements to make decisions in your code
  • Comparison operators (===, !==, <, >, <=, >=) compare values
  • Logical operators (&&, ||, !) combine conditions
  • for loops are best when you know how many times to repeat
  • while loops are best when you repeat until a condition changes
  • break exits a loop, continue skips to the next iteration

Next Steps

Congratulations! You've learned the fundamentals of control flow. Next, we'll explore data structures - how to work with collections of data like arrays and objects!

Pro Tip: When writing loops, always make sure your condition will eventually become false. Otherwise, you'll create an infinite loop that never stops! Also, practice reading other people's code to see different ways of solving problems with control flow.

Next lesson

Data Structures

Learn how to organize data with JavaScript arrays and objects. Covers destructuring, spread operator, and essential array methods.

18 min