In Dart 3, which switches must be exhaustive, and why does a switch on a bool? with only true and false cases fail to compile?
answer
- expressions always, statements sometimes
- bool, enums, sealed classes
- the nullable form adds null
- guards and field constraints cover nothing
- _ silences future omissions
basics
~20 sEvery Dart 3 switch expression must be exhaustive, as must a switch statement over bool, an enum, a sealed class or a nullable form of one. A bool? switch with only true and false leaves null unmatched, a compile error.
solid answer
~40 sDart 3 checks exhaustiveness from the scrutinee's **static type**. A switch **expression** is always checked, because it must yield a value. A switch **statement** is checked only when the type is an *exhaustive type* — `bool`, an enum, a `sealed` class, or a nullable version of one; a statement over `int` or `String` may leave values unmatched. `bool?` has three values, `true`, `false` and `null`, so cases for the first two leave `null` uncovered and the analyzer reports `non_exhaustive_switch_statement`. Fix it with a `case null:`, by switching on `flag ?? false`, or with `default`/`_`. Only unconditional patterns count: a case with a `when` guard or a constrained field such as `PaymentDeclined(code: 'expired')` covers nothing for the check, and a wildcard makes everything exhaustive but hides future values.
code
dart · 17 linesenum Plan { free, pro, team }
String label(Plan? plan) => switch (plan) {
Plan.free => 'Free',
Plan.pro => 'Pro',
Plan.team => 'Team',
null => 'No plan yet',
};
void logCode(int code) {
switch (code) {
case 200:
print('ok');
case 404:
print('missing');
}
}go deeper
Remember that a switch expression must cover every value and that a nullable scrutinee adds null to the values to cover.
Explain which types are exhaustive, why statements over int are exempt, and why guards and field-constrained patterns do not count as coverage.
Diagnose a non-exhaustive error from its message, choose between a null case and narrowing the scrutinee, and keep wildcards off enums and sealed types.
Weigh the maintenance value of compiler-proven coverage against convenience defaults, and set lint policy such as no_default_cases for the codebase.
## What exhaustiveness checking is **Exhaustiveness checking** is the Dart 3 compile-time analysis that reports an error when a value could enter a `switch` and match none of its cases. The analyzer works from the **static type** of the value being switched on (the *scrutinee*) and asks whether the union of all case patterns covers every value that type allows. Two diagnostics come out of it: - `non_exhaustive_switch_expression` — a switch **expression** misses some value; - `non_exhaustive_switch_statement` — a switch **statement** over an *exhaustive type* misses some value. Their message names what is missing, in the form: *The type 'X' isn't exhaustively matched by the switch cases since it doesn't match the pattern 'Y'.* For an enum from another library whose constants include private ones, the message says instead that some enum constants are private, because code outside that library cannot name them. ## Which switches are checked | Switch | Scrutinee type | Checked? | |---|---|---| | Expression | any type | **always** | | Statement | `bool`, an enum, a `sealed` class, or a nullable form of one | yes | | Statement | `int`, `String`, `Object`, an ordinary or `final` class | no — unmatched values run no case | A switch expression is always checked because it must produce a value; there is no case in which it can silently do nothing. A switch statement is checked only when the type's full set of values is knowable, which is exactly what `bool`, enums and `sealed` hierarchies provide. ## Why the bool? switch fails ```dart void describe(bool? optedIn) { switch (optedIn) { case true: print('yes'); case false: print('no'); } } ``` `bool?` is an exhaustive type, so this **statement** is checked. Its values are `true`, `false` **and `null`**, and `null` matches neither constant pattern. The fixes, in order of preference: 1. add a `case null:` (or `null =>` in an expression) that handles the missing value on purpose; 2. narrow the scrutinee first, for example by switching on `optedIn ?? false`, if `null` should mean `false`; 3. add `default:` or `_`, accepting that the compiler will no longer point at anything added later. The same holds for a nullable enum or a nullable sealed type: the `null` case is part of the space. ## What counts as covering a value A pattern covers a value only if it matches that value **unconditionally**. The usual reasons a switch that "has every case" is still rejected: - **a guard** — a case with `when` might not run, so it covers nothing for exhaustiveness purposes; - **a constrained field** — `PaymentDeclined(code: 'insufficient_funds')` covers one code, not every `PaymentDeclined`; - **a nullable scrutinee** — no `null` case; - **a non-sealed supertype** — an `abstract class` or `final class` is not an exhaustive type, so cases for every subclass you know about do not prove coverage; the compiler still asks for `_`; - **private enum constants** — an enum from another library with private constants cannot be covered by naming values. Conversely, `Type()` with no field constraints covers every instance of `Type`, including instances of its subtypes. ## A default is a trade `default` / `_` makes any switch exhaustive because it matches everything. On an open type such as `int` that is required for an expression. On an enum or a `sealed` type it is legal but costly: when a new constant or subtype appears, the wildcard absorbs it and the compiler has nothing to report. Two analyzer signals help here: - `unreachable_switch_default` flags a `default` in a statement can never run because earlier cases already cover every value; - the `no_default_cases` lint (opt-in) flags `default` in switch statements over enums and enum-like classes. ## Reading the error in practice When the build breaks with a non-exhaustive error after a dependency upgrade or a new enum value, work through it in this order: 1. Read the pattern in the message — it is the smallest case that is still missing, such as `PaymentCancelled()` or `null`. 2. Decide what that value should do at this call site; the error exists precisely because nobody has decided yet. 3. Add the case, or use the analyzer's "Add missing switch cases" quick fix, which inserts all of the missing cases at once, and fill in their bodies. 4. Only if every future value really should share one behaviour, add `_` — and accept that the compiler will stay silent next time. Treat the error as a checklist of places whose behaviour must be decided, not as noise to silence with `_`. On an open type such as `int` or `String` the opposite holds: a switch expression there always needs a wildcard, because no finite list of constants can cover the type.
- Why can't code in another library switch exhaustively over an enum that has private constants?Code outside the enum's library cannot name the private constants, so no set of cases it can write covers them. The analyzer reports that the enum isn't exhaustively matched because some constants are private; the caller needs a `_` or `default` case.
- Does a Dart switch over an abstract class with cases for every known subclass count as exhaustive?No. An ordinary `abstract` or `final` class can gain subtypes the compiler cannot enumerate from the switch's point of view, so it is not an exhaustive type. A switch expression over it still needs `_`; only a `sealed` supertype lets per-subtype cases prove coverage.
- Which analyzer diagnostic tells you a default in a Dart switch statement can never run?`unreachable_switch_default`: earlier cases already cover every value of the switched type, for example every constant of an enum, so the `default` is dead code and can be removed. Removing it also restores the compiler's ability to flag a newly added value.
saying these in an interview costs you the question
- A switch statement over int without a default is a compile error.
- Covering true and false is enough for a bool? switch.
- A guarded case counts toward exhaustiveness if its guard is usually true.
- Cases for every subclass make a switch over any abstract class exhaustive.
- A default case on an enum switch is always harmless.