skip to content

What is a guarded pattern (the `when` clause) in a switch, and how does it interact with case ordering and exhaustiveness?

level: middleimportance: must knowfreq 60%

answer

  1. case Pattern when <boolean> -> ...
  2. False guard = fall through to next case
  3. Guarded cases do NOT satisfy exhaustiveness
  4. Specific guards before the unguarded catch-all (dominance)
  5. `when` is a contextual keyword

basics

~20 s

A guarded pattern adds a boolean condition to a case using when, e.g. case Integer i when i > 0. The case matches only if the value fits the pattern AND the condition is true. If the guard is false, the switch keeps trying later cases.

solid answer

~50 s

A guard refines a pattern with an extra boolean test written after `when`. The case matches only when both the pattern matches and the guard expression (which can use the bound variable) evaluates to true; otherwise the switch falls through to subsequent cases. Because a false guard means "no match here," you almost always need a follow-up case for the same type without a guard (or a default) so the input is still covered. Critically, a guarded case does **not** count toward exhaustiveness for that type — the compiler can't prove the guard is always true, so a guarded `case Integer i when ...` alone won't make the switch exhaustive for integers. Order matters: put the more specific guarded cases before the broader catch-all of the same type, since the first matching case wins. Guards keep conditional logic readable inside the switch instead of nesting `if` statements in each branch.

code

java · 8 lines
java
static String classify(Object o) {
    return switch (o) {
        case Integer i when i < 0  -> "negative";
        case Integer i when i == 0 -> "zero";
        case Integer i             -> "positive";  // unguarded fallback
        default                    -> "not an int";
    };
}

go deeper

for a junior

Knows when adds a condition and the case matches only if both pattern and condition hold.

for a middle

Orders guarded cases correctly, knows a false guard falls through, and that guarded cases don't satisfy exhaustiveness so a fallback is needed.

for a senior

Explains dominance errors, exhaustiveness reasoning, contextual-keyword nature of when, and advises against side-effecting guards.

for a principal

Reasons about guards vs. extracting predicates into well-named methods, readability/maintainability trade-offs, and how guards fit data-oriented control flow over sealed hierarchies.

## Recap: type patterns A `switch` picks a branch by matching the *selector* value. A **type pattern** like `case Integer i` matches when the selector is an instance of `Integer` and binds it to `i`. (See the type-patterns question.) ## What a guard adds A **guarded pattern** attaches an extra **boolean condition** to a pattern using the contextual keyword `when`. Syntax: ```java case Integer i when i > 100 -> "big"; ``` The case matches **only if both** are true: 1. the pattern matches (the selector is an `Integer`, bound to `i`), **and** 2. the **guard** — the expression after `when`, here `i > 100` — evaluates to `true`. The guard may freely use the pattern's bound variable. If the pattern matches but the guard is `false`, this case is treated as **not matched**, and the switch continues evaluating the **following** cases in order. > `when` is a *contextual* keyword: it only has this meaning right after a pattern in a case label, so it does not break existing code that uses `when` as an identifier. ## Why ordering matters Cases are tested **top to bottom; the first match wins.** So specific guarded cases must come **before** a broader unguarded case of the same shape: ```java String classify(Object o) { return switch (o) { case Integer i when i < 0 -> "negative"; case Integer i when i == 0 -> "zero"; case Integer i -> "positive"; // catch-all for the rest default -> "not an int"; }; } ``` If you placed the unguarded `case Integer i` first, the guarded ones below it would be unreachable — and the compiler will reject a case that is **dominated** (made unreachable) by an earlier one. ## Guards and exhaustiveness **Exhaustiveness** means the switch must handle every possible input (required for pattern switches and all switch expressions). A **guarded** case does **not** contribute to exhaustiveness, because the compiler cannot prove the boolean guard is always true. Therefore: ```java // NOT exhaustive — only covers integers whose guard is true switch (o) { case Integer i when i > 0 -> ...; } ``` This fails to compile (for an expression or pattern switch) unless you add an **unguarded** fallback for the type and/or a `default`. The mental rule: *a guard can never be the thing that "closes off" a type — you still need an unconditional case or default.* ## Guard expression rules - The guard is any boolean expression; it can call methods, combine conditions with `&&`/`||`, and reference the bound variable(s). - Keep guards **side-effect-free**; a guard that mutates state or has costly side effects makes control flow hard to reason about. - A guard can throw — if it does, the switch propagates the exception. ## Deriving an answer Guard = pattern + boolean test. False guard = skip to next case. Because the compiler can't trust a guard to always hold, guarded cases never make a switch exhaustive, so you always pair them with an unguarded case or default; and because first-match-wins, specific guarded cases go above the general one.

  • Why won't `switch(o){ case Integer i when i>0 -> ...; }` compile as a switch expression over Object?
    It is not exhaustive: the guarded case only covers positive integers, and guards never count toward exhaustiveness. You need an unguarded `case Integer i` and/or a `default` to cover the remaining inputs.
  • What happens if a guarded case is placed after an unguarded case of the same type?
    The earlier unguarded case dominates it — every value of that type is already caught — so the guarded case is unreachable and the compiler reports a dominance error.

saying these in an interview costs you the question

  • Believing a guarded case alone makes the switch exhaustive for that type
  • Putting the unguarded case before guarded ones (causes dominance/unreachable error)
  • Thinking a false guard throws or jumps to default instead of trying the next case
  • Using side-effecting guards and assuming predictable evaluation

context