JavaScript Switch Statement: A Visual Guide

Aug 28, 2026·24 min read

The checkout status was "paid", but the notification code also sent the parcel message. One missing break let execution slide into the next case, and every individual line still looked reasonable.

A JavaScript switch statement evaluates one expression, selects the first strictly equal case in source order, and executes from that label until control leaves the switch or reaches its end.

What Is a JavaScript Switch Statement?

A switch statement is multi-branch control flow. You give it one controlling expression, then attach several possible entry points called case clauses.

The basic form has four parts:

  • switch (expression) evaluates the value used for matching.
  • case value: marks a possible entry point.
  • break exits the switch.
  • default: provides an entry point when no case matches.

Start with a complete status formatter:

const status = "paid";
let message;

switch (status) {
  case "draft":
    message = "Order is still being edited";
    break;
  case "paid":
    message = "Payment received";
    break;
  case "shipped":
    message = "Parcel is on the way";
    break;
  default:
    message = "Unknown order status";
}

console.log(message);
Payment received

JavaScript evaluates status, finds the first case whose expression matches "paid", and begins executing there. The assignment stores "Payment received", then break moves control to the first statement after the switch.

status“paid”matchcase draftcase paidcase shippedstore “Payment received”breakexit
A switch first finds an entry point, then follows the selected case body to an exit.

default is optional. Without it, a switch with no matching case does nothing.

This makes switch useful when several branches all answer the same question: which exact value does this expression hold? The dedicated switch statement lesson covers the core syntax alongside other Core JavaScript control-flow tools.

How Switch Matching Actually Works

A switch has a matching phase and an execution phase. Keeping them separate explains strict matching, source order, side effects in case expressions, and fall-through.

During the matching phase, JavaScript follows this order:

  1. Evaluate the controlling expression once.
  2. Evaluate case expressions from top to bottom.
  3. Compare each result with the controlling value using strict-equality semantics.
  4. Select the first match and stop evaluating later case expressions.
  5. Begin the execution phase at the selected label.

This annotated trace exposes both phases:

1. Evaluate `status` → `"paid"`

2. Evaluate the draft case expression → `"draft"`; compare → no match

3. Evaluate the paid case expression → `"paid"`; compare → match

4. Select the paid label; skip the shipped case expression

5. Run the paid body → fall through → run the shipped body → `break`

The code and console output show the same path:

function caseValue(label, value) {
  console.log(`matching: evaluate ${label}`);
  return value;
}

const status = "paid";

switch (status) {
  case caseValue("draft case", "draft"):
    console.log("execution: draft body");
    break;
  case caseValue("paid case", "paid"):
    console.log("execution: paid body");
  case caseValue("shipped case", "shipped"):
    console.log("execution: shipped body");
    break;
  default:
    console.log("execution: default body");
}
matching: evaluate draft case
matching: evaluate paid case
execution: paid body
execution: shipped body

The first two lines belong to matching. Once "paid" matches, JavaScript does not evaluate the "shipped" case expression.

The final two lines belong to execution. The missing break after the paid body lets execution continue into the statements under the shipped label, but that label is now an entry marker, not a condition JavaScript checks again.

Matching trackdraft?nopaid?matchshipped?not checkedenter hereExecution trackrun paid bodyno breakrun shipped bodythen break
Fall-through moves through bodies, not back through case comparisons.

Fall-through executes later statements; it does not restart matching.

Matching performs no type conversion. The number 1 and the string "1" may describe the same visible digit, but they are different values:

const choice = "1";

switch (choice) {
  case 1:
    console.log("number");
    break;
  case "1":
    console.log("string");
    break;
}
string

Only the string case matches. The same strict distinction appears throughout JavaScript comparisons, though switch uses it to choose an entry point rather than produce a Boolean result.

choice = “1”type: string===no matchmatchcase 1type: numbercase “1”type: string
The controlling string reaches the string case, not the numeric case.

Case expressions can be expressions rather than literals. That does not turn the statement into a lookup table: JavaScript still evaluates them in source order until it finds the first match.

Break, Default, and Fall-Through

break is the usual way out of a switch, but it is not the only one. return exits the surrounding function, and continue can move to the next iteration when the switch sits inside a loop.

This function returns directly from every branch:

function shippingLabel(method) {
  switch (method) {
    case "pickup":
      return "Collect from store";
    case "courier":
      return "Deliver to address";
    default:
      return "Choose a shipping method";
  }
}

console.log(shippingLabel("courier"));
Deliver to address

No break is needed because return has already left the function.

Inside a loop, continue can skip the rest of the current iteration:

const tasks = ["ready", "paused", "ready"];

for (const task of tasks) {
  switch (task) {
    case "paused":
      continue;
    case "ready":
      console.log("run task");
      break;
  }

  console.log("iteration finished");
}
run task
iteration finished
run task
iteration finished

For "paused", continue belongs to the surrounding for loop. It starts the next iteration, so "iteration finished" is not printed.

breakinsideswitchafter switchreturninsidefunctioncallercontinueinsideloopnext item
Terminating statements leave different enclosing structures.

Several empty labels can share one body. This is grouping via intentional empty-case fall-through:

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

console.log(isWeekend("Sunday"));
console.log(isWeekend("Tuesday"));
true
false

Both weekend labels enter the same return true body. There are no statements between them to run accidentally.

The bug-prone form of fall-through begins when a non-empty clause executes statements and then reaches another clause without terminating. Say a delivery fee starts at zero and each case adds its fee:

const zone = "local";
let fee = 0;

switch (zone) {
  case "local":
    fee += 5;
  case "remote":
    fee += 15;
    break;
  default:
    fee = 25;
}

console.log(fee);
20

The intended local fee was 5, but the result is a plausible 20. The "local" case adds 5, then execution falls through and adds the remote fee too.

Missing breakWith breaklocal: fee + 5falls throughremote: fee + 15fee = 20local: fee + 5breakfee = 5
One missing exit changes the path from a single charge to two accumulated charges.

Here is the complete corrected replacement:

const zone = "local";
let fee = 0;

switch (zone) {
  case "local":
    fee += 5;
    break;
  case "remote":
    fee += 15;
    break;
  default:
    fee = 25;
}

console.log(fee);
5

Intentional fall-through does have uses, but label it where it occurs:

const access = "owner";
const permissions = [];

switch (access) {
  case "owner":
    permissions.push("delete");
    // falls through
  case "editor":
    permissions.push("write");
    // falls through
  case "viewer":
    permissions.push("read");
    break;
}

console.log(permissions.join(", "));
delete, write, read

The owner receives each capability accumulated by the later clauses. The comments state that the missing termination is deliberate.

default may appear anywhere, though a switch can contain only one. If it appears before later cases and no case matches, execution starts at default and can fall through from there. Putting it last makes that behavior easier to see.

Four Switch Traps That Cause Real Bugs

Switch bugs tend to come from values that look alike, values strict equality cannot match, references that are not identical, or declarations whose scope is wider than the indentation suggests.

Mismatched types

User input often arrives as text. A string does not match a numeric case:

const floor = "2";

switch (floor) {
  case 2:
    console.log("second floor");
    break;
  default:
    console.log("no numeric match");
}
no numeric match

Convert at the boundary when the program requires a number, or write string cases when text is the intended value. Type Conversions explains the conversion step in detail.

NaN and signed zero

A case NaN clause cannot match because strict equality treats NaN as unequal to every value, including itself. By contrast, +0 and -0 are equal under strict equality, so separate zero cases cannot distinguish them.

NaNvalue: NaNcase NaNcannot matchSigned zero+0−0case 0both match
Strict matching separates NaN from itself but merges positive and negative zero.

This compact laboratory shows both results:

function classifyNumber(value) {
  switch (value) {
    case NaN:
      return "NaN case";
    case 0:
      return "zero case";
    default:
      return Number.isNaN(value) ? "detected NaN" : "other";
  }
}

console.log(classifyNumber(NaN));
console.log(classifyNumber(+0));
console.log(classifyNumber(-0));
detected NaN
zero case
zero case

Detect NaN before the switch or inside default. Use another test, such as Object.is, when signed zero genuinely needs separate handling. The Numbers lesson covers these numeric edge cases.

Object reference identity

Two object literals with the same properties are still two different references:

const savedFilter = { status: "paid" };
const activeFilter = { status: "paid" };

switch (activeFilter) {
  case savedFilter:
    console.log("same filter");
    break;
  default:
    console.log("different object");
}
different object

Matching an object works only when the switch value and case expression produce the same object reference. Dispatch on a stable property such as activeFilter.status when the branch depends on the object’s data.

Same contents{ status: “paid” }{ status: “paid” }ABA ≠ B: no matchSame referenceswitchvaluecasevalueone objectidentity CC === C: match
Identical-looking objects remain different unless both names point to the same object.

Shared lexical scope

Case clauses belong to one switch block. They do not each create a separate lexical scope, so repeating a const name in two unbraced cases causes a syntax error before the program runs.

Braces give each case its own block:

const command = "save";

switch (command) {
  case "save": {
    const message = "Document saved";
    console.log(message);
    break;
  }
  case "publish": {
    const message = "Document published";
    console.log(message);
    break;
  }
  default: {
    console.log("Unknown command");
  }
}
Document saved

Use the same braces around case-local let, function, and class declarations. The braces make the real scope match the visual structure.

Without bracesone switch scopecase saveconst messagecase publishconst messagename collisionWith bracesswitch{ case saveconst message }{ case publishconst message }two local scopes
Braces turn visually separate cases into genuinely separate lexical scopes.

Useful Switch Patterns

The cleanest switch often sits inside a function and returns one result. Every branch terminates, and the caller does not need a mutable variable outside the statement:

function formatPriority(priority) {
  switch (priority) {
    case "low":
      return "Can wait";
    case "normal":
    case "high":
      return "Add to queue";
    case "urgent":
      return "Handle now";
    default:
      return "Invalid priority";
  }
}

console.log(formatPriority("high"));
Add to queue

Grouping handles values that share behavior. A status or discriminant property handles objects whose shape selects the operation:

function reduceOrder(order) {
  switch (order.status) {
    case "draft":
      return `Edit order ${order.id}`;
    case "paid":
      return `Pack order ${order.id}`;
    case "shipped":
      return `Track order ${order.id}`;
    default:
      return `Review order ${order.id}`;
  }
}

console.log(reduceOrder({ id: 42, status: "shipped" }));
Track order 42

Here status is the discriminant: one property whose value identifies which branch applies. This shape also works well when each case delegates to a separate function, a small form of the Strategy pattern.

switch (true) changes the question. Each case expression produces a Boolean, and the first expression equal to true wins:

function ticketBand(age) {
  switch (true) {
    case age < 5:
      return "free";
    case age < 18:
      return "child";
    case age >= 65:
      return "senior";
    default:
      return "adult";
  }
}

console.log(ticketBand(12));
child

Order matters because age < 5 is also covered by age < 18. This is ordered Boolean-condition matching, not ordinary dispatch on one domain value. Use it when the ordered case layout reads better than the corresponding if...else; it is not the default replacement for if...else. The individual conditions follow the rules in Logical operators.

age = 12age < 5falseage < 18truereturn childlater testsare skipped
For age 12, the first false test is passed and the first true test wins.

Switch vs If, Object Lookups, and Map

Choose the construct by the kind of decision the code makes.

ConstructUse it whenExample question
switchSeveral exact branches share one controlling value and each branch performs control flowWhich order status is this?
if...elseBranches use ranges, compound tests, or unrelated Boolean conditionsIs the account locked, or is the balance too low?
Object lookupSimple string keys select data with no branch-specific control flowWhich label belongs to this code?
MapData-driven keys may have arbitrary value typesWhich handler belongs to this object reference?

A lookup object is shorter when the entire job is retrieving data:

const labels = {
  draft: "Still editing",
  paid: "Ready to pack",
  shipped: "On the way",
};

const status = "paid";
console.log(Object.hasOwn(labels, status) ? labels[status] : "Unknown status");
Ready to pack

A switch earns its space when branches validate, return, continue, call different operations, or intentionally share execution. Use if...else when forcing several unrelated conditions into case expressions hides the decision.

Do not choose between them by assuming one is inherently faster. Engine behavior and the actual workload determine performance. Choose the form that states the selection rule accurately.

Writing Maintainable Switch Statements

A maintainable switch makes each entry point, scope, and exit visible. Apply the same rules whether the statement has three cases or thirty:

  • Wrap a case body in braces when it declares const, let, a function, or a class.
  • End each active case explicitly with break, return, throw, or an applicable continue.
  • Mark every deliberate fall-through with a nearby comment.
  • Handle unexpected values in default, unless ignoring them is a deliberate part of the function’s contract.
  • Keep the controlling expression focused on one named value or discriminant.
  • Move large case bodies into named functions before the switch becomes a wall of implementation details.

ESLint’s no-case-declarations rule reports lexical declarations placed directly in case clauses without braces. In ESLint 10.9.1, the recommended @eslint/js configuration enables it, so the braced style works with the rule and makes the scope accurate on the page.

ESLint’s no-fallthrough rule reports a case that can run into the next one. In ESLint 10.9.1, the recommended @eslint/js configuration enables it, and the rule recognizes comments marking intentional fall-through. These two rules turn the most common visual ambiguities into automated checks. Broader naming and layout choices belong with the site’s Coding style guidance.

Optional TypeScript exhaustiveness

TypeScript can check a switch over a discriminated union by assigning the remaining value to never in default.

Here is the pattern:

type Order =
  | { status: "draft"; id: number }
  | { status: "paid"; id: number }
  | { status: "shipped"; id: number };

function nextAction(order: Order): string {
  switch (order.status) {
    case "draft": {
      return `Edit order ${order.id}`;
    }
    case "paid": {
      return `Pack order ${order.id}`;
    }
    case "shipped": {
      return `Track order ${order.id}`;
    }
    default: {
      const unhandled: never = order;
      return unhandled;
    }
  }
}

After the three known statuses have been handled, TypeScript narrows order to never. If the union later gains another status and the switch does not gain a matching case, the assignment reports that the switch is no longer exhaustive. This example was checked with TypeScript 7.0.2; the TypeScript handbook documents this never-based exhaustiveness check.

Order status uniondraftpaidshippednew:refundedexisting case branchesall handledno casedefault:neverTypeScript reportserror“never” should receive nothingso arrival reveals the omission
A new union member exposes a missing case by reaching the supposedly unreachable never branch.

The JavaScript execution model has not changed. TypeScript adds a check before the emitted program runs.

Once exact matching, fall-through, and case-local scope feel predictable, the same control-flow rules become easier to recognize inside larger functions. JavaScript Fundamentals collects the surrounding lessons on expressions, conditions, loops, functions, and the language rules those structures share.

Frequently asked questions

How does a switch statement work in JavaScript?
JavaScript evaluates the switch expression once, then checks case expressions in source order using strict-equality semantics. Execution begins at the first matching case, or at default when no case matches.
Does JavaScript switch use strict equality?
Yes. Switch matching follows strict-equality semantics and does not convert types, so the number 1 does not match the string "1". NaN cannot match a case NaN clause, while +0 and -0 are treated as equal.
What happens if I omit break from a switch case?
Execution continues into the statements under later case labels until a terminating statement or the end of the switch is reached. This behavior is called fall-through, and it happens without evaluating those later case expressions.
Can JavaScript case clauses declare const or let variables?
They can, but all clauses share the switch statement's lexical scope. Wrap each case body in braces when it declares const, let, a function, or a class so the declaration has a case-local block.
When should I use switch instead of if else?
Use switch for several exact branches selected from one value, especially when each branch performs control flow. Use if...else for ranges or unrelated Boolean conditions, an object for simple string-keyed data, and Map for data-driven keys of arbitrary value types.