skip to content

What does exhaustiveness mean for a switch expression, and how does the compiler enforce it?

level: seniorimportance: must knowfreq 58%

answer

  1. Expression must produce a value => must be exhaustive
  2. default makes any switch exhaustive
  3. Enum: list all constants => no default needed
  4. Synthetic guard throws on unknown new constant at runtime
  5. Omit default to get compile errors when constants are added

basics

~10 s

A switch expression must cover every possible input value. You either add a 'default' branch or list all values (e.g. every enum constant). If you don't, it won't compile.

solid answer

~50 s

Because a switch expression must always produce a value, the compiler requires it to be exhaustive — every possible input must match some branch. The simplest way is a 'default' branch. When switching over an enum, you may instead list all enum constants; the compiler then accepts it as exhaustive without a default. There's a subtlety: even an exhaustive enum switch with no default can fail at runtime if the enum class gains a new constant after compilation, so the compiler inserts a synthetic check that throws (an IncompatibleClassChangeError / MatchException-style error) for the unknown value. For sealed types and pattern-label switches, the compiler likewise verifies all permitted subtypes or patterns are covered. Best practice is to prefer exhaustive enum/sealed switches without a catch-all default, so adding a new constant or subtype produces a compile error you must handle, rather than silently hitting default.

go deeper

for a junior

Knows a switch expression must cover all cases, and that adding 'default' satisfies that.

for a middle

Explains that listing all enum constants is exhaustive without default and that omitting a case won't compile.

for a senior

Reasons about the separate-compilation runtime guard and deliberately omits default on enum/sealed switches to get compile-time coverage checks.

for a principal

Establishes team policy: exhaustive enum/sealed switches over catch-all defaults, anticipating evolution of closed type hierarchies and the maintainability tradeoffs, and understands the MatchException/ICCE guard semantics.

## Why exhaustiveness exists A switch **expression** must evaluate to a value on *every* execution. Therefore there can be no input that matches *no* branch — that would leave the expression with no value. This property is called **exhaustiveness**: the set of branches must cover the entire domain of the selector. ## The two ways to be exhaustive **1. A `default` branch.** A `default` matches anything not matched above, so any switch expression with a `default` is automatically exhaustive: ```java String s = switch (n) { case 1 -> "one"; case 2 -> "two"; default -> "many"; // covers everything else }; ``` **2. Cover every constant of an enum (or every permitted type of a sealed type).** When the selector is an enum, the compiler knows the *complete* finite set of constants. If you list them all, the switch is exhaustive **without** a `default`: ```java enum Size { SMALL, MEDIUM, LARGE } int cost = switch (size) { // no default — all 3 constants listed case SMALL -> 1; case MEDIUM -> 2; case LARGE -> 3; }; ``` Omit one constant and it **won't compile**. ## The hidden runtime guard There is a separation-compilation subtlety. Suppose `Size` is compiled, your switch is compiled against three constants, then later someone recompiles `Size` to add `HUGE` but does **not** recompile the switch. At runtime `size` could now be `HUGE` — a value your switch never anticipated. To keep the must-produce-a-value guarantee, the compiler inserts a **synthetic default** that throws an error (historically an `IncompatibleClassChangeError`; with newer pattern switches a `MatchException`) instead of returning garbage. So an "exhaustive at compile time" enum switch is still protected at runtime. ## Sealed types and patterns The same idea extends to **sealed** types (a class/interface that restricts which types may extend or implement it) and to **pattern** labels (Java 21+). The compiler knows the closed set of permitted subtypes, so a switch covering all of them is exhaustive without a `default`: ```java sealed interface Shape permits Circle, Square {} double area = switch (shape) { case Circle c -> Math.PI * c.r() * c.r(); case Square s -> s.side() * s.side(); }; ``` ## Design guidance: avoid a catch-all default If you add a `default` to an enum/sealed switch, the compiler can no longer warn you when a new constant or subtype is introduced — the new case silently lands in `default`. By **omitting** `default` and listing all cases, adding a new constant/subtype causes a **compile error** at every switch that must change. This turns "I forgot to handle the new state" from a lurking runtime bug into an immediate, located compile failure — a powerful maintainability property. Reserve `default` for genuinely open domains (e.g. switching on an `int` or `String`).

  • Why is omitting 'default' on an enum switch sometimes the better choice?
    Without a catch-all default, adding a new enum constant makes every must-handle switch fail to compile until updated. This surfaces missing cases at compile time instead of silently routing them to default at runtime.
  • Does a switch *statement* require exhaustiveness like a switch expression?
    No. A classic switch statement may legally ignore some inputs (do nothing). Only switch *expressions* must be exhaustive because they must always yield a value.

saying these in an interview costs you the question

  • Claiming a switch expression compiles without covering all inputs
  • Always adding 'default' to enum switches (hides future-constant compile errors)
  • Assuming an exhaustive enum switch can never fail at runtime (separate-compilation guard can throw)
  • Confusing switch statement (no exhaustiveness requirement) with switch expression (exhaustiveness required)

context