# JavaScript Control Flow: if, else, and switch Explained

**Control flow** is the order in which your code runs. By default, JavaScript runs line by line, top to bottom. Conditional statements let your code **make decisions**: run this block only if something is true; otherwise, run that one.

This guide covers `if`, `if...else`, `else if`, `switch` and the ternary operator. When to use each, with step-by-step flows, examples, and the gotchas that cause real bugs.

## What goes inside the condition?

Every `if` needs a condition that JavaScript treats as `true` or `false`.

**Comparison operators** (return a boolean)

| Operator | Meaning | Example |
| --- | --- | --- |
| `===` | Strictly equal (value and type) | `5 === 5` is `true`, `5 === "5"` is `false` |
| `!==` | Strictly not equal | `5 !== 6` is `true` |
| `>` `<` `>=` `<=` | Greater / lesser | `age >= 18` |

**Logical operators** (combine conditions)

| Operator | Meaning | Example |
| --- | --- | --- |
| `&&` | AND: both must be true | `age >= 18 && hasId` |
| `||` | OR: at least one is true | `isAdmin || isOwner` |
| `!` | NOT: flips the value | `!isLoggedIn` |

**Truthy and falsy.** If you put a non-boolean in a condition, JavaScript converts it. Only these 8 values are **falsy**:

`false`, `0`, `-0`, `0n`, `""`, `null`, `undefined`, `NaN`

Everything else is truthy, including `"0"`, `[]`, and `{}`.

```javascript
if ("hello") { console.log("runs"); }   // non-empty string is truthy
if (0) { console.log("never runs"); }   // 0 is falsy
if ([]) { console.log("runs"); }        // empty array is truthy!
```

> **Rule:** use `===` instead of `==`. Strict equality never converts types behind your back.

## 1\. `if` (single decision)

**Flow:**

1.  Start
    
2.  Check the condition
    
3.  If `true`, run the block. If `false`, skip it.
    
4.  Continue with the rest of the code
    

```javascript
const age = 20;

if (age >= 18) {
  console.log("You can vote");   // runs
}
console.log("Done");             // always runs
```

## 2\. `if...else` (two paths)

**Flow:**

1.  Check the condition
    
2.  `true` → run the `if` block
    
3.  `false` → run the `else` block
    
4.  Exactly **one** of the two blocks runs
    

```javascript
const age = 15;

if (age >= 18) {
  console.log("Adult");
} else {
  console.log("Minor");   // runs
}
```

## 3\. `if...else if...else` (many paths)

**Flow:**

1.  Check the first condition. If`true`, run its block and **stop**.
    
2.  Otherwise, check the next condition, and repeat.
    
3.  If none are true, run the `else` block.
    
4.  Only **one** block ever runs.
    

```javascript
const score = 85;

if (score >= 90) {
  console.log("A");
} else if (score >= 80) {
  console.log("B");   // runs
} else if (score >= 70) {
  console.log("C");
} else {
  console.log("Fail");
}
```

**Gotcha: <mark class="bg-yellow-200 dark:bg-yellow-500/30">order matters</mark>.** Conditions are checked from the top, so put the most specific one first.

```javascript
// Wrong: 90 is caught by the first check, so "A" can never run
if (score >= 60) { console.log("Pass"); }
else if (score >= 90) { console.log("A"); }
```

## 4\. `switch` (match exact values)

**Flow:**

1.  Evaluate the expression once
    
2.  Compare it with each `case` using **strict equality (**`===`**)**
    
3.  On a match, run that case's code until a `break`
    
4.  If nothing matches, run `default`
    

```javascript
const day = 3;

switch (day) {
  case 1:
    console.log("Monday");
    break;
  case 2:
    console.log("Tuesday");
    break;
  case 3:
    console.log("Wednesday");   // runs
    break;
  default:
    console.log("Another day");
}
```

### Gotcha 1: forgetting `break` causes fall-through

Without `break`, JavaScript keeps running the next cases too.

```javascript
switch (1) {
  case 1:
    console.log("one");   // runs
  case 2:
    console.log("two");   // also runs! (no break above)
    break;
  default:
    console.log("other");
}
// one
// two
```

### Gotcha 2: `switch` uses strict matching

```javascript
switch ("1") {
  case 1:
    console.log("number one");
    break;
  default:
    console.log("no match");   // runs, because "1" !== 1
}
```

### Trick: group cases that share code

Fall-through is useful on purpose when cases share the same action.

```javascript
switch (month) {
  case "Dec":
  case "Jan":
  case "Feb":
    console.log("Winter");
    break;
  case "Jun":
  case "Jul":
  case "Aug":
    console.log("Summer");
    break;
}
```

### Gotcha 3: `let` and `const` inside cases

All cases share one block, so declaring the same name twice fails. Wrap the case in `{ }`.

```javascript
switch (type) {
  case "a": {
    const msg = "Type A";
    console.log(msg);
    break;
  }
  case "b": {
    const msg = "Type B";   // fine, separate block
    console.log(msg);
    break;
  }
}
```

### Ranges don't work in `switch`

`switch` compares exact values, so `case x > 10` does not work as you would expect. Use `if...else if` for ranges.

## 5\. Ternary operator (a short `if...else`)

**Syntax:** `condition ? valueIfTrue : valueIfFalse`

*   **Returns:** one of the two values, so you can store it.
    

```javascript
const age = 20;
const status = age >= 18 ? "Adult" : "Minor";   // "Adult"

console.log(`You are an ${age >= 18 ? "adult" : "minor"}`);
```

> **Rule:** use it for simple choices. Avoid chaining several ternaries, since that becomes hard to read.

## 6\. Short-circuit and `??` (default values)

`&&` and `||` stop as soon as the result is known, and they **return one of the values**, not just `true` or `false`.

```javascript
const user = { name: "Sam" };

user && user.name;       // "Sam"  (runs only if user exists)
const name = "" || "Guest";    // "Guest"  (|| falls back for ANY falsy value)
```

**Gotcha:** `||` treats `0` and `""` as "missing", which can be wrong.

```javascript
const count = 0;
count || 10;     // 10  (bug: 0 is a valid value)
count ?? 10;     // 0   (correct)
```

`??` **(<mark class="bg-yellow-200 dark:bg-yellow-500/30">nullish coalescing</mark>)** only falls back for `null` and `undefined`. Use it for defaults.

## 7\. Guard clauses: avoid deep nesting

Check the bad cases first and exit early. Your code stays flat and easy to read.

```javascript
// Hard to read
function checkout(user) {
  if (user) {
    if (user.isLoggedIn) {
      if (user.cart.length > 0) {
        return "Proceed";
      }
    }
  }
  return "Cannot checkout";
}

// Easier: guard clauses
function checkout(user) {
  if (!user) return "Cannot checkout";
  if (!user.isLoggedIn) return "Cannot checkout";
  if (user.cart.length === 0) return "Cannot checkout";
  return "Proceed";
}
```

## Which one should I use?

| Statement | Type of flow | Use it for |
| --- | --- | --- |
| `if` | One decision | One condition |
| `if...else` | Two-way | Either A or B |
| `if...else if...else` | Many conditions | Ranges and complex logic |
| `switch` | Multi-branch match | Exact fixed values |
| Ternary `? :` | Inline two-way | Simple value choice |
| Object lookup | Key-value mapping | Many fixed values (see below) |

### `if...else` vs `switch`

| Factor | `if...else` | `switch` |
| --- | --- | --- |
| Handles ranges (`x > 10 && x < 20`) | Yes | No |
| Handles complex logic | Yes | No |
| Matches exact values | Yes | Yes (designed for it) |
| Readability with many cases | Gets long | Cleaner |
| Comparison | Whatever you write | Always `===` |

> **Use** `if...else` **when:** conditions involve ranges or logic, expressions are complex, or you have only a few branches.

> Use switch when: you match exact values, you have many cases, and you want a clean structure.

## What about performance?

Honest answer: **it seldom matters.**

*   **Few conditions (about 5 or fewer):** no real difference. Choose readability.
    
*   **Many fixed values (10+ cases):** a `switch` *can* be faster, because engines can optimize it (for example, into a jump table for dense integer cases). This is not guaranteed.
    
*   **Strings and sparse values:** optimization is less predictable. Expect roughly the same speed as `if...else`.
    
*   **Typical difference:** microseconds, far smaller than a single network request or DOM update.
    

A long `if...else` chain checks conditions one by one, so in the worst case it does as many checks as there are branches. That cost is tiny for normal code. **<mark class="bg-yellow-200 dark:bg-yellow-500/30">Measure before you optimize</mark>.**

## Better than both: object lookup

When you are just mapping a **value to another value**, skip the branching entirely.

```javascript
// switch version
function dayName(n) {
  switch (n) {
    case 1: return "Monday";
    case 2: return "Tuesday";
    case 3: return "Wednesday";
    default: return "Unknown";
  }
}

// object lookup version
const days = { 1: "Monday", 2: "Tuesday", 3: "Wednesday" };
const dayName = (n) => days[n] ?? "Unknown";

dayName(2);    // "Tuesday"
dayName(9);    // "Unknown"
```

Why it is good: lookup by key is fast, the code is shorter, and adding a case is just adding a line.

**You can store functions too:**

```javascript
const actions = {
  add: (a, b) => a + b,
  sub: (a, b) => a - b,
};

actions["add"]?.(2, 3);      // 5
actions["power"]?.(2, 3);    // undefined (no crash)
```

**Gotcha: inherited keys.** A plain object has built-in keys like `toString` and `constructor`, so a lookup can return something you did not define.

```javascript
days["toString"];    // a function, not undefined!
```

**Safer options:**

```javascript
Object.hasOwn(days, key) ? days[key] : "Unknown";   // own keys only

const dayMap = new Map([[1, "Monday"], [2, "Tuesday"]]);
dayMap.get(1) ?? "Unknown";                          // Map has no inherited keys
```

Object lookup only works for **<mark class="bg-yellow-200 dark:bg-yellow-500/30">exact-value mapping</mark>**. For ranges or complex conditions, use `if...else`.

## Common mistakes

| Mistake | Fix |
| --- | --- |
| `if (x = 5)` (assignment, always truthy) | Use `if (x === 5)` |
| Using `==` | Use `===` |
| Forgetting `break` in `switch` | Add `break`, or comment on intentional fall-through |
| Putting a broad condition first in `else if` | Order from most specific to least |
| Using `||` for defaults when `0` or `""` is valid | Use `??` |
| Long nested `if` blocks | Use guard clauses |
| Using `switch` for ranges | Use `if...else if` |
| Looking up keys in a plain object blindly | Use `Object.hasOwn()` or a `Map` |

## Wrap up

*   Control flow is the order your code runs. Conditions let it choose a path.
    
*   Use `if...else if...else` for ranges and logic, `switch` for exact values (it uses `===`, and needs `break`).
    
*   Use the **ternary** for quick value choices, `??` for defaults, and **guard clauses** to avoid deep nesting.
    
*   Performance differences between `if` and `switch` are tiny. Choose what reads best.
    
*   For plain value-to-value mapping, an **object lookup** or a `Map` is the cleanest option.
    

Next up: loops (`for`, `while`, `for...of`), the other half of control flow.
