Control Flow: The Art of Digital Decision Making

Control Flow: The Art of Digital Decision Making

Control flow is the "brain" of your JavaScript application. It determines the order in which statements are executed, which paths are taken, and which are ignored based on the state of your data. Without control flow, programs would be rigid, linear scripts incapable of responding to user input or environmental changes.

This chapter explores how to guide your code through complex logic, avoid "spaghetti code," and write branches that are both performant and easy for humans to read.


Why This Topic Matters

Control flow isn't just about if statements; it's about program architecture. Mastering it will allow you to:

  • Handle Complexity: Manage multi-step processes like user registration or checkout flows.
  • Enhance Performance: Understand how the engine optimizes different branching strategies.
  • Reduce Cognitive Load: Write code that is "scannable"—where a developer can understand the logic at a glance.
  • Ensure Robustness: Gracefully handle edge cases and "unhappy paths" using early exits.

The Mental Model: Decision Trees

Think of your program as a series of forks in a road. At each fork, an expression is evaluated to a Boolean (true or false), determining which path the execution pointer follows.

Condition?TRUEExecute AFALSEExecute BPaths rejoin after the block


Primary Branching: if, else if, and else

The if statement is the most fundamental unit of control flow.

The Logic of Exclusive Branches

In an if / else if / else chain, only one block will ever execute. Once a condition is met, JavaScript skips the rest of the chain.

const temperature = 25;

if (temperature > 30) {
  console.log("It's hot!");
} else if (temperature > 20) {
  console.log("It's pleasant."); // This runs
} else {
  console.log("It's cold.");     // This is skipped
}

Under the Hood: The Cost of Chaining

Every else if is an additional comparison. If you have 50 else if statements and the match is at the end, the engine performs 50 checks. For high-performance scenarios with many exact-match cases, switch or Lookup Tables (discussed later) are often preferred.


Pattern Matching with switch

The switch statement evaluates an expression and matches its result against a series of case labels.

Strict Comparison

switch uses Strict Equality (===). This means "5" will not match 5.

const role = "admin";

switch (role) {
  case "admin":
    console.log("Full Access");
    break;
  case "editor":
    console.log("Can Edit");
    break;
  default:
    console.log("Guest");
}

The "Fallthrough" Feature

If you omit the break, execution "falls through" to the next case regardless of whether the condition matches.

Intentional Fallthrough:

const fruit = "lime";
switch (fruit) {
  case "lemon":
  case "lime":
  case "orange":
    console.log("This is a citrus fruit.");
    break;
}

Advanced Pattern: The Lookup Table

When you have many exact-match conditions, an object can act as a more readable and performant "Lookup Table."

const getDrinkPrice = (drink) => {
  const prices = {
    "coffee": 3.00,
    "tea": 2.50,
    "water": 1.50
  };

  return prices[drink] ?? "Drink not found";
};

console.log(getDrinkPrice("coffee")); // 3.00

Why this is better:

  1. O(1) Complexity: Objects use hash maps, making the lookup nearly instant regardless of the number of items.
  2. Cleaner Syntax: No break or case keywords cluttering the logic.
  3. Data-Driven: You can easily move these prices to a JSON file or database.

Truthy and Falsy: The Hidden Coercion

JavaScript's control flow works on "truthiness," not just literal true/false.

The "Naughty List" (Falsy Values)

There are only six values that resolve to false in a condition:

  1. false
  2. 0 (and -0, 0n)
  3. "" (Empty string)
  4. null
  5. undefined
  6. NaN (Not a Number)

Everything else is truthy, including [] (empty array) and {} (empty object).

The Danger of 0

let score = 0;

if (score) { 
  // This will NOT run, because 0 is falsy!
}

Fix: Be explicit. Use if (score !== undefined) or if (typeof score === 'number').


The "Guard Clause" Pattern

Nested if statements create a "Pyramid of Doom" that makes code hard to follow. The Guard Clause flips this logic by handling the "bad" cases first and exiting early.

Bad: Nested Pyramid

function processOrder(order) {
  if (order) {
    if (order.items.length > 0) {
      if (order.isPaid) {
        // Main logic here
        return "Success";
      } else {
        return "Not Paid";
      }
    } else {
      return "Empty Order";
    }
  } else {
    return "No Order";
  }
}

Good: Guard Clauses

function processOrder(order) {
  if (!order) return "No Order";
  if (order.items.length === 0) return "Empty Order";
  if (!order.isPaid) return "Not Paid";

  // Main logic here (Nice and flat!)
  return "Success";
}

Logical Short-Circuiting

You can use && and || to perform "micro-control flow" without if statements.

The Guarding &&

Only executes the right side if the left side is truthy.

user.isLoggedIn && showDashboard();

The Defaulting ||

Uses the right side if the left side is falsy.

const name = inputName || "Anonymous";

The Nullish ??

Uses the right side only if the left side is null or undefined (ignores 0 or "").

const volume = settings.volume ?? 50; // Keeps volume if it's 0

The Ternary Operator: Inline Decisions

Syntax: condition ? valueIfTrue : valueIfFalse

It is best used for simple assignments. If you find yourself nesting ternaries, stop and use a standard if or a function.

// Clean
const access = user.isAdmin ? "Full" : "Limited";

// Messy (Avoid this!)
const color = isRed ? "red" : isBlue ? "blue" : isGreen ? "green" : "white";

Common Mistakes & Pitfalls

  1. Missing break: Accidentally falling through and executing multiple switch cases.
  2. Confusing || and ??: Overwriting valid 0 or "" values with defaults.
  3. Floating Point Logic: if (0.1 + 0.2 === 0.3) will be false. Never use float math in conditions.
  4. Assignment vs Comparison: Writing if (x = 5) (assigns 5, always true) instead of if (x === 5).

Mini Exercises

  1. The Bouncer: Create a function that takes an age and returns "Enter" if 18+, and "Go home" otherwise.
  2. Refactor Challenge: Take a 4-level nested if statement and rewrite it using Guard Clauses.
  3. The Switcher: Write a switch statement that converts "1", "2", "3" into "Gold", "Silver", "Bronze".
  4. Lookup Table: Create an object that maps "Monday" to "Gym", "Wednesday" to "Coding", and "Friday" to "Netflix". Write a function to access it safely.
  5. Short-Circuit: Use && to call console.log("Hello") only if a variable isReady is true.

Review Questions

  1. Which specific values in JavaScript are considered "falsy"?
  2. Why is a "Lookup Table" often better than a long else if chain?
  3. How does switch compare values? (Abstract or Strict equality?)
  4. What is the main benefit of using "Guard Clauses" over nested if statements?
  5. When should you use ?? instead of ||?

Reference Checklist

  • I can list all 6 falsy values from memory.
  • I understand when to use break in a switch statement.
  • I can refactor nested code into flat guard clauses.
  • I know how to use an object as a lookup table.
  • I understand the difference between || and ?? for default values.