In a Dart 3 switch, what happens when a case's when guard evaluates to false, and how do guarded cases affect exhaustiveness?
answer
- match first, then test
- false means try the next case
- pattern variables are in scope
- guards prove nothing to the checker
- specific guarded case above the plain one
basics
~20 sA when guard is checked after its pattern matches and binds variables; if it is false, matching continues with the next case instead of leaving the switch. Guarded cases never count toward exhaustiveness, so an unguarded case must still cover that value.
solid answer
~50 sA `when` guard follows a case pattern in a switch statement, a switch expression or an `if-case`. Dart first matches the pattern and binds its variables, then evaluates the guard with those variables in scope. If the guard is `true` the case is taken; if it is `false`, matching **falls through to the next case** rather than exiting the switch, which is what an `if` inside a statement body cannot do. Because a guard is an arbitrary runtime condition, the exhaustiveness checker treats a guarded case as covering nothing, so the usual idiom is a guarded, specific case followed by an unguarded case for the same type, for example `PaymentSucceeded(:var amountCents) when amountCents >= 100000` then `PaymentSucceeded()`. Order matters, pattern variables cannot be assigned inside the guard, and one guard can be shared by a logical-or pattern.
code
dart · 26 linessealed class PaymentResult {
const PaymentResult();
}
final class PaymentSucceeded extends PaymentResult {
const PaymentSucceeded(this.amountCents);
final int amountCents;
}
final class PaymentDeclined extends PaymentResult {
const PaymentDeclined();
}
// Error: non_exhaustive_switch_expression.
// PaymentResult isn't exhaustively matched: it doesn't match 'PaymentSucceeded()'.
String badLabel(PaymentResult r) => switch (r) {
PaymentSucceeded(:var amountCents) when amountCents >= 100000 => 'Large',
PaymentSucceeded(:var amountCents) when amountCents < 100000 => 'Normal',
PaymentDeclined() => 'Declined',
};
String goodLabel(PaymentResult r) => switch (r) {
PaymentSucceeded(:var amountCents) when amountCents >= 100000 => 'Large',
PaymentSucceeded() => 'Normal',
PaymentDeclined() => 'Declined',
};go deeper
Know that when adds a condition to a case and that a false guard sends matching on to the next case.
Explain the match, bind, test order, why guarded cases do not count toward exhaustiveness, and why the plain case must come after the guarded one.
Spot guards that duplicate relational patterns, hide business rules or carry side effects, and restructure them into patterns, records or named predicates.
Set review guidance on when conditional logic belongs in guards versus in the model, keeping switches readable and their coverage provable.
## What a guard is A **guard clause** is a boolean condition written after a case pattern with the keyword `when`. It is allowed on switch statement cases, switch expression cases and `if-case` statements: ```dart sealed class PaymentResult { const PaymentResult(); } final class PaymentSucceeded extends PaymentResult { const PaymentSucceeded(this.amountCents); final int amountCents; } final class PaymentDeclined extends PaymentResult { const PaymentDeclined(this.retryable); final bool retryable; } String headline(PaymentResult result) => switch (result) { PaymentSucceeded(:var amountCents) when amountCents >= 100000 => 'Large payment received', PaymentSucceeded() => 'Payment received', PaymentDeclined(:var retryable) when retryable => 'Try again', PaymentDeclined() => 'Payment declined', }; ``` ## How a guarded case is evaluated 1. The **pattern** is matched first. If it fails, the case is skipped. 2. If it matches, the pattern's variables are **bound** — here `amountCents` or `retryable`. 3. The **guard** is evaluated with those variables in scope. 4. If the guard is `true`, the case body runs (or, in an expression, becomes the result). 5. If the guard is `false`, matching **continues with the next case**. The switch is not exited. That last step is the whole point of a guard. An `if` inside a statement case body cannot do it: once a case has been selected, a failing `if` just leaves the body having done nothing, and the switch ends. A switch expression has no body statements at all, so a guard is the only way to add a non-pattern condition to one of its cases. ## Guards and exhaustiveness Exhaustiveness checking asks whether some value could reach the end of the switch unmatched. A guard is an arbitrary runtime expression, so the compiler cannot prove it true for any value; a guarded case is therefore treated as covering **nothing** when the compiler decides whether the switch is exhaustive. | Cases for `PaymentSucceeded` | Exhaustive for that subtype? | |---|---| | `PaymentSucceeded() when ...` only | no — error names `PaymentSucceeded()` | | guarded case, then `PaymentSucceeded()` | yes | | `PaymentSucceeded(amountCents: >= 0)` only | no — a field constraint is not total either | | guarded case, then `_` at the end | yes, but `_` also hides future subtypes | The two guards on complementary conditions do not help either: `when amountCents >= 100000` and `when amountCents < 100000` together cover every `int`, but the compiler does not reason about guard expressions, so it still wants an unguarded case. The idiom is the one used above: the guarded, more specific case first, then the unguarded pattern for the same type. Dart 3's own release example follows it: `_ when page == _lastPage => 'Start over'` is followed by a plain `_ => 'Previous page'`. ## Rules that trip people up - **Order matters.** Cases are tried top to bottom. If the unguarded `PaymentSucceeded()` came first, the guarded case below it would never run. - **Scope.** Variables bound by the pattern are visible in the guard and the body of that case only. - **No assignment to pattern variables in a guard.** `when (amountCents = 0) > 0` is the compile-time error `pattern_variable_assignment_inside_guard`. - **One guard per case.** With a logical-or pattern, one guard applies to every alternative: `case Square(size: var s) || Circle(size: var s) when s > 0:` — the or-branches must bind the same variables with the same types. - **Guards may have side effects, but should not.** A guard can run for a value and then fail, so side effects in guards make behaviour depend on case order in ways that are hard to review. ## When to prefer something else - If the condition is a constant comparison, a **relational pattern** (`>= 100000`) inside the pattern is clearer than a guard, and a relational pattern on a field still counts as a constraint, not total coverage. - If many cases share one complex condition, compute it once before the switch and switch on a record, for example `switch ((result, isLarge))`. - If a guard grows into business logic, move that logic into a named function and call it from the guard. A useful review question for any guard is: *what happens to a value that matches this pattern but fails this condition?* If the answer is "it falls into a later case I did not intend", reorder the cases or add an explicit unguarded case for the same pattern right after the guarded one, so that the fallback is visible in the code rather than implied by case order further down.
- In a Dart switch statement, why is a when guard different from an if at the top of the case body?With an `if` in the body, the case has already been chosen; when the condition is false the body does nothing and the switch ends. With a guard, a false condition means the case never matched, so Dart tries the following cases and can still select one of them.
- Can one Dart when guard apply to several alternative patterns?Yes, with a logical-or pattern: `case Square(size: var s) || Circle(size: var s) when s > 0:`. Each alternative must bind the same variables with the same types, so the guard can use `s` whichever side matched.
saying these in an interview costs you the question
- A false when guard exits the whole switch.
- Two guards on complementary conditions make a switch exhaustive.
- A guard runs before the pattern is matched.
- A guard may reassign the variables its pattern bound.
- Case order does not matter once guards are added.