skip to content

Explain case-label dominance ordering and the MatchException in pattern switches. When does each arise?

level: seniorimportance: should knowfreq 40%

answer

  1. dominance = compile-time ordering, no unreachable arms
  2. most specific first; general/default last
  3. unguarded dominates later guarded same-type arm
  4. MatchException = runtime, exhaustiveness broken
  5. cause: separate compilation adds a subtype, switch not recompiled
  6. synthetic default throws it; don't swallow it

basics

~20 s

Dominance means a more general case must come after the more specific ones it would otherwise swallow; putting it first is a compile error because later arms become unreachable. MatchException is a runtime error thrown when an exhaustive-looking switch meets a value no arm covers, usually due to mismatched separate compilation.

solid answer

~50 s

Dominance is a compile-time ordering rule: if an earlier case label would match every value a later label could, the later one is unreachable and the compiler rejects it. So a supertype pattern (or unguarded pattern) must be placed after the more specific subtype or guarded patterns it dominates; case null and constants have their own ordering rules. MatchException is a runtime exception (java.lang.MatchException, Java 21) the compiler's synthetic default throws when a switch that was proven exhaustive at compile time nevertheless meets a value no arm matches. The usual cause is separate compilation: a sealed hierarchy gains a new subtype but the switch's class isn't recompiled, so the closed-set assumption is violated at runtime. It can also surface from a record deconstruction over a null component in certain nested cases. Dominance is about authoring order at compile time; MatchException is the loud failure when the exhaustiveness guarantee is broken at runtime rather than a silent wrong answer.

go deeper

for a junior

Knows you must put specific cases before general ones or the code won't compile, and that MatchException is a runtime error for an unhandled value.

for a middle

Explains dominance prevents unreachable arms at compile time and that MatchException comes from the synthetic default when no arm matches at runtime.

for a senior

Distinguishes dominance (compile-time ordering), exhaustiveness (compile-time coverage), and MatchException (runtime), and identifies separate-compilation skew as the main MatchException cause.

for a principal

Builds processes (clean/modular rebuilds, versioning discipline) so sealed-hierarchy skew never reaches production, and reasons about MatchException as a fail-loud guarantee in evolvable APIs rather than something to catch.

## Dominance (a compile-time rule) A pattern switch evaluates labels **top to bottom**, taking the first that matches. **Dominance** is the rule that an earlier label must not make a later one **unreachable**. If label A would match every value that label B could match, A *dominates* B, and placing B after A is a **compile error** ("label is dominated by a preceding case"). Typical cases: ```java // ERROR: CharSequence dominates String (every String is a CharSequence) switch (obj) { case CharSequence cs -> ...; case String s -> ...; // unreachable -> compile error } ``` Correct order is **most specific first**: ```java switch (obj) { case String s -> ...; // specific case CharSequence cs -> ...; // more general, after default -> ...; } ``` The same applies to **guards**: an unguarded `case Integer i` dominates a later `case Integer i when ...`, because the unguarded arm catches all `Integer`s. So guarded (more specific) arms go *before* the unguarded fallback for the same type. `default` is conceptually last (it matches anything left); `case null` is special and not subject to the usual dominance ordering since only the literal `null` reaches it. ### Why dominance exists It prevents dead code and silent logic bugs: without it, you could write an unreachable arm and never notice. The compiler makes the mistake impossible. ## MatchException (a runtime error) **`MatchException`** (`java.lang.MatchException`, new in Java 21) is thrown at **runtime** when a pattern switch that the compiler proved **exhaustive** is nonetheless handed a value that **no arm matches**. The compiler inserts a hidden **synthetic default** in exhaustive switches whose body throws `MatchException` — a safety net, not something you write. ### When it actually fires 1. **Separate-compilation skew (most common).** You compile a sealed `interface Shape permits Circle, Square` and a switch covering both. Later someone adds `Triangle` to `permits` and recompiles *only* the `Shape` file, not the switch's class. At runtime a `Triangle` reaches the switch; no arm matches; the synthetic default throws `MatchException`. This converts a silent "falls through to nothing" into a loud, debuggable failure. 2. **Record deconstruction with null components.** In some nested record patterns, a `null` component that the deconstruction can't bind leads to a `MatchException` rather than an NPE, per the matching semantics. ### Why it's good Before `MatchException`, an impossible-seeming gap would be undefined or silently wrong. Now you get a specific, catchable exception type signaling "the exhaustiveness assumption was violated" — almost always pointing at a build/version problem. ## Contrast at a glance - **Dominance** = *compile-time*, about **ordering** of labels, prevents unreachable arms. - **MatchException** = *runtime*, about **coverage breaking** after compile-time exhaustiveness, usually from separate compilation. ## Practical guidance - Order labels **most specific → most general**; put guarded arms before the unguarded fallback; let the compiler catch mistakes. - Don't catch `MatchException` to paper over it — treat it as a signal to rebuild everything that depends on the changed sealed hierarchy. - Prefer building all dependents together (clean rebuilds, modular builds) so the skew never happens in production.

  • Why does case CharSequence cs before case String s fail to compile?
    Every String is a CharSequence, so the CharSequence arm matches all Strings first; the String arm is unreachable. The compiler reports the dominated label. Order String before CharSequence.
  • Your exhaustive sealed switch throws MatchException in production though it compiled cleanly. What's the most likely cause?
    Separate-compilation skew: a new permitted subtype was added to the sealed hierarchy and that file recompiled, but the switch's class was not. At runtime an uncovered value reaches the synthetic default, which throws MatchException. Rebuild all dependents.

saying these in an interview costs you the question

  • Placing a supertype or unguarded pattern before the specific ones (compile error, not just a warning).
  • Thinking MatchException is a normal control-flow tool to catch — it signals a broken exhaustiveness/build assumption.
  • Confusing dominance (compile-time ordering) with exhaustiveness (compile-time coverage) or with MatchException (runtime).
  • Assuming an exhaustive switch can never throw at runtime.

context