JavaScript Switch

5 minute read

switch is JavaScript’s pattern for dispatching on a value across many possible cases. It’s clean for command-style dispatchers and state machines, but it has two big footguns: fall-through and strict equality only matching. This chapter covers the syntax, the gotchas, and the lookup-object pattern that’s often a better choice.

Basic Syntax

const role = "admin";

switch (role) {
  case "admin":
    console.log("Full access");
    break;
  case "editor":
    console.log("Can edit");
    break;
  case "viewer":
    console.log("Read only");
    break;
  default:
    console.log("No access");
}
// Output: Full access

The expression in switch (...) is evaluated once and compared with each case value using strict equality (===) — the same rules as === covered in chapter 24.

The Fall-Through Trap

Without break, execution falls through to the next case:

const day = "Sat";

switch (day) {
  case "Sat":
    console.log("weekend");
    // ⚠ no break
  case "Sun":
    console.log("relax");
    break;
  default:
    console.log("weekday");
}
// Output:
// weekend
// relax

This is sometimes intentional — but more often a bug. ESLint’s no-fallthrough rule flags it.

Warning: Always end every case with break, return, or throw. If you genuinely want fall-through, put a comment // fall through so reviewers (and future you) know it’s deliberate.

Intentional Fall-Through — Grouping Cases

The one acceptable use of fall-through: grouping cases that share the same body.

function isWeekend(day) {
  switch (day) {
    case "Sat":
    case "Sun":
      return true;
    default:
      return false;
  }
}

isWeekend("Sat");   // true
isWeekend("Mon");   // false

Stacking cases with no body in between is the idiomatic way to say “any of these”.

Default — Position Doesn’t Matter (But Convention Does)

switch (x) {
  default:
    console.log("other");
    break;
  case 1:
    console.log("one");
    break;
}

Legal, but unreadable. Convention: put default last.

If default is missing and nothing matches, the switch is a no-op:

switch (1) {
  case 2: console.log("two"); break;
}
// (no output)

Returning From Switch

When every case ends in return, you don’t need break:

function describe(role) {
  switch (role) {
    case "admin":  return "Administrator";
    case "editor": return "Editor";
    case "viewer": return "Viewer";
    default:       return "Unknown";
  }
}

This is the cleanest form for a function whose whole job is dispatch. No break, no fall-through risk.

Strict Equality — Beware of Type

Cases match with ===, never ==. This bites when comparing input that’s a string vs a number:

const status = "200";   // string — maybe from an API or URL

switch (status) {
  case 200:               // number 200 — never matches!
    console.log("OK");
    break;
  case "200":             // matches
    console.log("OK as string");
    break;
}
// Output: OK as string
switch (NaN) {
  case NaN:   // ❌ never matches — NaN !== NaN
    console.log("nan");
    break;
}

Switching on Ranges — Use true Trick (or Don’t)

switch matches values, not predicates. To dispatch on ranges you need a workaround:

function grade(score) {
  switch (true) {                        // expression is `true`
    case score >= 90: return "A";
    case score >= 80: return "B";
    case score >= 70: return "C";
    default:          return "F";
  }
}

Each case is then a boolean expression: the first one that equals true matches. It works but it’s clever — many style guides ban it. An if/else if chain is usually clearer:

function grade(score) {
  if (score >= 90) return "A";
  if (score >= 80) return "B";
  if (score >= 70) return "C";
  return "F";
}

Block Scoping Inside Cases

By default, all cases share one lexical scope. Declaring let/const in two cases collides:

switch (x) {
  case 1:
    const msg = "one";   // ❌ SyntaxError if any other case also declares msg
    break;
  case 2:
    const msg = "two";
    break;
}

Wrap each case body in braces to give it its own scope:

switch (x) {
  case 1: {
    const msg = "one";
    console.log(msg);
    break;
  }
  case 2: {
    const msg = "two";
    console.log(msg);
    break;
  }
}

Tip: Get into the habit of always wrapping case bodies in {} if they declare variables. It costs you two characters and saves a category of bug.

When switch Is Better — and When It Isn’t

Situation Use
Dispatch on a single discrete value to one of N branches switch or lookup object
Branches all just return a value lookup object is clearer
Branches do non-trivial work switch (with return form)
Conditions are ranges or compound predicates if/else if
Polymorphic behaviour by type classes / methods

The Lookup Object Alternative

For data-style dispatch, an object is shorter, declarative, and has no fall-through risk:

const ROLE_LABEL = {
  admin:  "Administrator",
  editor: "Editor",
  viewer: "Viewer",
};

function describe(role) {
  return ROLE_LABEL[role] ?? "Unknown";
}

When the branches do work (not just return data), use a function map:

const HANDLERS = {
  click:    e => console.log("clicked at", e.x, e.y),
  keydown:  e => console.log("key", e.key),
  scroll:   e => console.log("scrolled"),
};

function dispatch(eventType, event) {
  const handler = HANDLERS[eventType] ?? (() => console.log("unknown event"));
  handler(event);
}

A lookup object is also easier to extend at runtime — just add a key.

A Realistic Example — Reducer Style

A small state machine with a switch:

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM": {
      const items = [...state.items, action.item];
      return { ...state, items, total: state.total + action.item.price };
    }

    case "REMOVE_ITEM": {
      const items = state.items.filter(i => i.id !== action.id);
      const removed = state.items.find(i => i.id === action.id);
      const total = removed ? state.total - removed.price : state.total;
      return { ...state, items, total };
    }

    case "CLEAR_CART":
      return { ...state, items: [], total: 0 };

    default:
      return state;          // unknown action → no change
  }
}

// Usage
let state = { items: [], total: 0 };
state = cartReducer(state, { type: "ADD_ITEM", item: { id: 1, price: 10 } });
state = cartReducer(state, { type: "ADD_ITEM", item: { id: 2, price: 5  } });
console.log(state.total);   // 15

Each case has its own brace-scoped block, returns immediately, and a sensible default keeps unknown actions safe.

Common Mistakes

// ❌ Forgetting break
switch (x) {
  case 1: console.log("one");        // falls through!
  case 2: console.log("two"); break;
}

// ❌ Type mismatch with strict equality
switch ("1") {
  case 1: /* never */ break;
}

// ❌ Variable name collision across cases
switch (x) {
  case 1: const v = "a"; break;
  case 2: const v = "b"; break;   // SyntaxError
}

// ❌ Missing default — silent no-op for unknown values
switch (status) {
  case "ok": done(); break;
  case "err": fail(); break;
}
// what about other statuses?

Best Practices Checklist

  • ✅ Always include break, return, or throw at the end of every case
  • ✅ Always include a default (even if it just returns the unchanged state)
  • ✅ Wrap case bodies in {} when declaring variables
  • ✅ Convert switches that just map values to a lookup object
  • ✅ Coerce types before switching (switch (Number(status))) when input is mixed
  • ❌ Don’t use switch (true) for ranges — use if/else if
  • ❌ Don’t fall through implicitly without a // fall through comment
  • ❌ Don’t reach for switch for two cases — if/else is fine

What’s Next?

You’ve covered branching. Time for the other half of control flow: looping. The classic for, the modern for…of, and the array iteration helpers all working in concert.

← Previous: JS Conditionals Next: JS Loops (for) →

Updated: