Skip to main content

🚦 Control Structures: Conditionals & Loops

A program that always runs the same lines in the same order is little more than a fancy calculator tape. Control structures are the traffic signals of your code β€” they let it choose which path to take and repeat work as many times as needed. This is where JavaScript stops merely storing data and starts making decisions.

Week 1 · Day 5 (Friday: Introduction to JavaScript) · Lecture 2

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Branch your code with if, else if, else, and the switch statement
  • Choose the ternary operator for concise inline decisions and know when not to
  • Use short-circuiting, nullish coalescing, and optional chaining to handle missing values safely
  • Repeat work with for, while, and do...while loops
  • Iterate collections cleanly with for...of and objects with for...in
  • Steer loops precisely with break, continue, and labels

Estimated Time: 75 minutes

Practice: Build a plus/minus grade calculator and a number-guessing game loop.

In This Lesson

Control Flow, Explained

Imagine reading a recipe. Most of the time you follow the steps top to bottom β€” that's sequential flow. But some steps say "if the dough is too sticky, add flour" (a decision) and others say "knead for ten minutes" (a repetition). Those two ideas β€” selection and iteration β€” are the entirety of control flow, and every program you'll ever write is built from them.

graph TD A[Program Start] --> B{Condition?} B -->|True| C[Execute Path A] B -->|False| D[Execute Path B] C --> E[Continue] D --> E E --> F{Loop again?} F -->|Yes| G[Repeat Actions] G --> F F -->|No| H[Program End]

The diamonds are decisions; the loop-back arrow is repetition. Master these two shapes and you can express any logic a computer can run. Let's take them one at a time.

Conditional Statements

A conditional runs a block of code only when a condition is truthy. The workhorse is if, optionally paired with else if and else to build a chain of mutually exclusive choices.

if / else if / else

// A single if: run the block only when the test is true
const age = 18;
if (age >= 18) {
    console.log("You are an adult");
}

// if + else: exactly one of the two branches runs
if (age >= 18) {
    console.log("You are an adult");
} else {
    console.log("You are a minor");
}

// A chain: JavaScript checks top-to-bottom and stops at the FIRST match
const score = 85;
if (score >= 90) {
    console.log("Grade: A");
} else if (score >= 80) {
    console.log("Grade: B");   // ← matches here, so the rest are skipped
} else if (score >= 70) {
    console.log("Grade: C");
} else {
    console.log("Grade: F");
}

Why the order matters: the chain evaluates conditions in sequence and executes the first one that is true. That's why the wide net (>= 60) goes last β€” put it first and every passing score would be graded "D."

Flatten nesting with logical operators

Deeply nested if statements are hard to read. When several conditions must all hold, combine them with && instead of stacking braces.

const age = 25, hasLicense = true, hasInsurance = true;

// πŸ‘Ž Nested β€” the "arrow of doom" grows to the right
if (age >= 18) {
    if (hasLicense) {
        if (hasInsurance) {
            console.log("You can drive!");
        }
    }
}

// πŸ‘ Flat β€” combine conditions, and give each failure its own message
if (age >= 18 && hasLicense && hasInsurance) {
    console.log("You can drive!");
} else if (age < 18) {
    console.log("You must be 18 or older to drive.");
} else if (!hasLicense) {
    console.log("You need a license to drive.");
} else if (!hasInsurance) {
    console.log("You need insurance to drive.");
}

πŸ“– Truthy & falsy

An if doesn't need a literal true/false β€” it coerces whatever it's given. The falsy values are exactly eight: false, 0, -0, 0n, "", null, undefined, and NaN. Everything else β€” including "0", [], and {} β€” is truthy. Knowing this list prevents a surprising number of bugs.

The switch Statement

When you're comparing one value against many fixed options, a long if/else if chain gets noisy. switch reads more cleanly for that shape. It compares the subject to each case using strict equality (===).

const day = "Monday";

switch (day) {
    case "Monday":
        console.log("Start of the work week");
        break;                         // ← stop here; without it, execution "falls through"
    case "Tuesday":
    case "Wednesday":
    case "Thursday":
        console.log("Middle of the work week");   // shared body for 3 cases
        break;
    case "Friday":
        console.log("End of the work week");
        break;
    case "Saturday":
    case "Sunday":
        console.log("Weekend!");
        break;
    default:
        console.log("Not a valid day");   // runs when nothing else matches
}

⚠️ Don't forget break

Without a break, JavaScript keeps running the next case's body too β€” this is called "fall-through." Sometimes that's intentional (grouping Saturday/Sunday above), but a forgotten break is a classic bug. The default case is your safety net for unexpected values.

The switch(true) trick for ranges

switch tests equality, not ranges β€” but you can flip it: switch on true and put the comparison in each case.

const score = 85;
switch (true) {
    case score >= 90: console.log("Grade: A"); break;
    case score >= 80: console.log("Grade: B"); break;   // first true case wins
    case score >= 70: console.log("Grade: C"); break;
    default:          console.log("Grade: F");
}

Ternary & Short-Circuiting

The ternary operator β€” a decision that returns a value

Where if runs statements, the ternary operator produces a value, so it fits neatly on the right side of an assignment. Its shape is condition ? valueIfTrue : valueIfFalse.

const age = 20;
const status = age >= 18 ? "adult" : "minor";   // "adult"

// Chaining works but gets hard to read β€” use sparingly:
const grade = score >= 90 ? "A"
            : score >= 80 ? "B"
            : score >= 70 ? "C"
            : "F";
πŸ’‘ Rule of thumb: Use a ternary when you're choosing a value. Use an if when you're performing an action. Cramming side effects into a ternary hurts readability fast.

Short-circuiting & safe access

The logical operators don't just return true/false β€” they return one of their operands, and they stop early once the result is decided. That behavior powers three everyday patterns.

// && stops at the first falsy value β€” a guard against calling on null:
const user = null;
user && user.getName();   // short-circuits; getName() never runs

// || returns the first truthy value β€” classic "default" fallback:
const name = user?.name || "Guest";

// ?? (nullish coalescing) only defaults on null/undefined, NOT on 0 or "":
const count = 0;
const shown = count ?? 10;   // 0  ← keeps a legitimate zero (|| would give 10)

// ?. (optional chaining) reads deep paths without throwing:
const profile = { address: { street: "123 Main St" } };
console.log(profile.address?.street);   // "123 Main St"
console.log(profile.phone?.number);     // undefined β€” no "cannot read" error

// Combine them for a bullet-proof read with a default:
const street = profile.address?.street ?? "No address provided";

βœ… || vs ??

Reach for ?? whenever 0, "", or false are valid values you want to keep. || treats all of them as "missing" and replaces them β€” a subtle bug when zero is a real answer.

Loops

Loops repeat a block of code. Which loop you choose depends on what you know up front about how many times to repeat.

Three loop shapes: for when the count is known, while when it depends on a condition, do...while when it must run at least once for count is known init; test; step πŸ”’ fixed range while test THEN body may run 0 times πŸ”„ condition-driven do…while body THEN test always runs β‰₯ 1 βœ… prompt-then-check
Pick for for a known count, while when repetition depends on a condition, and do...while when the body must run at least once (like prompting for input).

for β€” when you know the count

// The three parts: initializer; condition; step
for (let i = 0; i < 5; i++) {
    console.log(i);   // 0, 1, 2, 3, 4
}

// Walk an array by index (classic, but for...of below is cleaner)
const fruits = ["apple", "banana", "orange"];
for (let i = 0; i < fruits.length; i++) {
    console.log(fruits[i]);
}

// Count backwards
for (let i = fruits.length - 1; i >= 0; i--) {
    console.log(fruits[i]);   // orange, banana, apple
}

// Nested loops build grids β€” here, a times table
for (let i = 1; i <= 3; i++) {
    for (let j = 1; j <= 3; j++) {
        console.log(`${i} x ${j} = ${i * j}`);
    }
}

while β€” repeat while a condition holds

// Test first, then run the body β€” so it can run zero times
let count = 0;
while (count < 5) {
    console.log(count);
    count++;              // ← ALWAYS move toward the exit, or you loop forever
}

// Great when the number of iterations isn't known in advance
let password = "";
while (password !== "secret123") {
    password = prompt("Enter the password:");
}
console.log("Access granted!");

do...while β€” run once, then check

// The body runs BEFORE the first test, so it executes at least once
let n = 0;
do {
    console.log(n);
    n++;
} while (n < 5);

// Perfect for input you must ask for at least once and re-validate
let userAge;
do {
    userAge = parseInt(prompt("Enter your age (a positive number):"), 10);
} while (Number.isNaN(userAge) || userAge <= 0);
console.log("Your age is:", userAge);

⚠️ The infinite loop

Every while/do...while needs something inside that eventually makes the condition false. Forget to increment a counter or update the flag and the loop never ends, freezing the tab. When you do want to loop "forever," use while (true) with a deliberate break inside.

Iterating Collections

Modern JavaScript gives you two purpose-built loops for stepping through data. Choosing the right one is mostly about arrays versus objects.

for...of β€” values of an iterable (use for arrays)

const fruits = ["apple", "banana", "orange"];
for (const fruit of fruits) {
    console.log(fruit);   // the VALUES: apple, banana, orange
}

// Works on strings, Sets, Maps β€” anything iterable
for (const char of "Hi") console.log(char);   // H, i

// Pair it with destructuring for tidy code
const users = [["John", 30], ["Jane", 25]];
for (const [name, age] of users) {
    console.log(`${name} is ${age} years old`);
}

const scores = new Map([["math", 90], ["art", 85]]);
for (const [subject, mark] of scores) {
    console.log(`${subject}: ${mark}`);
}

for...in β€” keys of an object

const person = { name: "John", age: 30, city: "New York" };
for (const key in person) {
    console.log(`${key}: ${person[key]}`);   // name: John, age: 30, city: New York
}

πŸ’‘ for...in vs for...of

Use for...in for object keys and for...of for array/iterable values. Avoid for...in on arrays: it yields string indices ("0", "1"…) and can pick up inherited properties. When you need both index and value from an array, use for (const [i, v] of arr.entries()).

break & continue

Two keywords let you override a loop's normal rhythm: break exits the loop entirely, and continue skips to the next iteration.

// break: stop the loop the moment we've found what we need
for (let i = 0; i < 10; i++) {
    if (i === 5) break;   // leaves the loop completely
    console.log(i);       // 0, 1, 2, 3, 4
}

// continue: skip just this pass, keep looping
for (let i = 0; i < 5; i++) {
    if (i === 2) continue;   // skip 2
    console.log(i);          // 0, 1, 3, 4
}

// Labels let break/continue target an OUTER loop
outer: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (i === 1 && j === 1) break outer;   // exits BOTH loops
        console.log(i, j);
    }
}

Labels are rare in day-to-day code β€” most nested-loop exits are cleaner when the logic is pulled into a function that can simply return. But when you need them, they're the tidy way to escape more than one level.

Common Loop Patterns

A handful of loop shapes come up again and again. Recognizing them by name makes you faster β€” and later, you'll swap many of them for array methods like find, filter, and reduce.

Search, filter, accumulate

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// SEARCH β€” find an element, then stop early
let foundIndex = -1;
for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] === 7) { foundIndex = i; break; }
}
console.log(foundIndex);   // 6

// FILTER β€” collect the items that pass a test
const evens = [];
for (const num of numbers) {
    if (num % 2 === 0) evens.push(num);
}
console.log(evens);        // [2, 4, 6, 8, 10]

// ACCUMULATE β€” fold the array into a single value
let sum = 0;
for (const num of numbers) sum += num;
console.log(sum);          // 55

A real validator

Here loops and conditionals team up. Note the pattern of returning early on the first failure β€” a "guard clause" that keeps the happy path unindented.

function validatePassword(password) {
    if (password.length < 8) {
        return "Password must be at least 8 characters long";
    }

    let hasUpper = false, hasLower = false, hasNumber = false, hasSpecial = false;
    for (const char of password) {
        if (char >= "A" && char <= "Z") hasUpper = true;
        else if (char >= "a" && char <= "z") hasLower = true;
        else if (char >= "0" && char <= "9") hasNumber = true;
        else if ("!@#$%^&*".includes(char)) hasSpecial = true;
    }

    if (!hasUpper)   return "Password must contain an uppercase letter";
    if (!hasLower)   return "Password must contain a lowercase letter";
    if (!hasNumber)  return "Password must contain a number";
    if (!hasSpecial) return "Password must contain a special character";
    return "Password is valid";
}

console.log(validatePassword("weak"));         // too short
console.log(validatePassword("Str0ng!Pass"));  // "Password is valid"

Output

validatePassword("weak")        β†’ "Password must be at least 8 characters long"
validatePassword("Str0ng!Pass") β†’ "Password is valid"

Practice & Quiz

πŸ‹οΈ Exercise 1: Grade calculator with +/βˆ’

Goal: Write letterGrade(score) that returns a grade like "B+" or "A-", and returns "Invalid" for scores outside 0–100.

function letterGrade(score) {
    // TODO: guard invalid input, then map the score to a letter + modifier
}
console.log(letterGrade(97));  // "A+"
console.log(letterGrade(83));  // "B"
console.log(letterGrade(150)); // "Invalid"
πŸ’‘ Hint

Guard first: if (Number.isNaN(score) || score < 0 || score > 100) return "Invalid";. Find the base letter with an if/else if chain, then decide the modifier from the last digit of the score (roughly: >= 7 is +, <= 2 is -). A caps at +; F has no modifier.

βœ… Solution
function letterGrade(score) {
    if (Number.isNaN(score) || score < 0 || score > 100) return "Invalid";

    let letter;
    if (score >= 90) letter = "A";
    else if (score >= 80) letter = "B";
    else if (score >= 70) letter = "C";
    else if (score >= 60) letter = "D";
    else return "F";               // F never gets a +/-

    const ones = score % 10;
    const mod = ones >= 7 ? "+" : ones <= 2 ? "-" : "";
    // Cap at A+ (no A++) and don't emit A- above 90 boundaries awkwardly
    if (letter === "A" && mod === "-") return "A";
    return letter + mod;
}
console.log(letterGrade(97));  // "A+"
console.log(letterGrade(83));  // "B"
console.log(letterGrade(150)); // "Invalid"

πŸ‹οΈ Exercise 2: Number-guessing loop

Goal: Simulate the guessing logic without prompts. Write guessesToFind(secret, start) that uses a loop to count how many +1 steps it takes to reach secret from start.

βœ… Solution
function guessesToFind(secret, start) {
    let guess = start;
    let attempts = 0;
    while (guess !== secret) {
        guess++;
        attempts++;
        if (guess > 1000) break;   // safety valve against a runaway loop
    }
    return attempts;
}
console.log(guessesToFind(42, 40));  // 2
console.log(guessesToFind(7, 7));    // 0 (already there)

The real interactive version swaps the guess++ for a prompt() and prints "higher"/"lower" hints β€” but the loop skeleton is identical.

🎯 Quick Quiz

Question 1: In a switch, what happens if you forget break at the end of a matching case?

Question 2: Which loop is guaranteed to run its body at least once?

Question 3: Which loop should you use to read the values of an array?

Best Practices & Pitfalls

βœ… Do

  • Use guard clauses (early return) to flatten deep nesting
  • Reach for for...of on arrays and for...in on object keys
  • Extract complex conditions into a well-named function, e.g. canDrive(user)
  • Always ensure a loop can reach its exit condition
  • Prefer switch when matching one value against many fixed options

❌ Don't

  • Nest if statements three or four levels deep β€” refactor instead
  • Forget break in a switch (unless you truly want fall-through)
  • Cram side effects and multiple actions into a ternary
  • Loop over arrays with for...in β€” the indices are strings and order isn't guaranteed

⚠️ Guard clauses beat the pyramid

// πŸ‘Ž Pyramid of doom
function canDrive(user) {
    if (user.age >= 18) {
        if (user.hasLicense) {
            if (!user.suspended) return true;
        }
    }
    return false;
}

// πŸ‘ Flat and readable
function canDrive(user) {
    if (user.age < 18) return false;
    if (!user.hasLicense) return false;
    if (user.suspended) return false;
    return true;
}

Same logic, far less indentation. Each condition reads as a plain sentence, and there's no rightward drift.

Summary

πŸŽ‰ Key Takeaways

  • Conditionals branch your code; an if/else if chain runs the first true block and stops
  • switch is cleaner for one-value-many-options, but remember your breaks
  • The ternary chooses a value; ||, ??, and ?. handle defaults and missing data
  • Pick the loop by what you know: for (count), while (condition), do...while (at least once)
  • Use for...of for array values, for...in for object keys, and steer with break/continue

πŸ“š Additional Resources

πŸš€ What's Next?

You can now make decisions and repeat work β€” but as your logic grows, you'll want to package it into reusable, named blocks. The next lesson introduces the single most important building block in the language: Functions and Scope.

πŸŽ‰ Great progress!

Your programs can now think and repeat. That's the beating heart of every algorithm you'll ever write.