skip to content

Switch Expressions & Exhaustiveness

Switch expressions return a value from pattern cases, and the compiler rejects a switch that misses a sealed subtype or an enum value. Interviewers ask how sealed classes model sum types.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Dart 3, how does a switch expression differ from a switch statement, and where may a switch expression appear?

level: juniorimportance: must knowfreq 62%

answer

  1. a value versus control flow
  2. => bodies, commas, no case keyword
  3. only _ as the default
  4. never at the start of a statement
  5. the expression form must cover everything

basics

~20 s

A Dart 3 switch expression yields a value: cases are pattern => expression, comma-separated, _ is the only default, and it must be exhaustive. A switch statement runs statements and is exhaustiveness-checked only on types like enums, bool and sealed classes.

solid answer

~40 s

A switch **expression** evaluates to the value of its matching case. Its cases drop the `case` keyword, use `pattern => expression`, are separated by commas, each must have a body, and the only catch-all is `_`. It can go anywhere an expression can — an initializer, a `return`, a function or widget argument — except at the start of an expression statement, where `switch (` parses as a statement. It must be **exhaustive**, or the analyzer reports `non_exhaustive_switch_expression`. A switch **statement** runs one or more statements per case, allows `default:` and empty cases that share a body, and is only checked for exhaustiveness when it switches over an exhaustive type such as `bool`, an enum or a `sealed` class. Both forms accept any pattern and `when` guards.

code

dart · 14 lines
dart
enum Plan { free, pro, team }

String badge(Plan plan) => switch (plan) {
  Plan.free => 'FREE',
  Plan.pro || Plan.team => 'PAID',
};

void main() {
  final label = switch (DateTime.now().weekday) {
    DateTime.saturday || DateTime.sunday => 'weekend',
    _ => 'weekday',
  };
  print('${badge(Plan.pro)} on a $label');
}

go deeper

for a junior

Recall the shape: no case keyword, pattern => expression, commas between cases, _ as the only default. Be able to rewrite a simple enum switch statement as an expression.

for a middle

Explain why the expression form must be exhaustive while a statement over int need not be, and how logical-or patterns replace empty shared cases.

for a senior

Show when to choose each form: expressions for value-producing branches that the compiler can prove complete, statements for side effects, and resisting a reflexive _ on enums.

for a principal

Argue for team conventions such as preferring exhaustive switch expressions over if-else chains on enums and sealed types, and enabling lints that flag defaults hiding new values.

## Two forms of the same pattern switch Since **Dart 3.0**, every `case` in Dart holds a **pattern**, and the language offers two shapes of `switch` built on the same matching machinery: - a **switch statement** — control flow: each matching case runs a list of statements and nothing is produced; - a **switch expression** — a value: each matching case evaluates one expression, and the whole `switch (...) { ... }` evaluates to that result. Both match cases top to bottom, both accept any pattern kind, both allow a `when` guard after a pattern, and in both the variables a pattern binds are in scope only inside that case's body. What differs is the syntax, where the construct may be written, and how strictly the compiler demands that every value be covered. ## Syntax side by side | Aspect | Switch statement | Switch expression | |---|---|---| | Case keyword | `case pattern:` | no `case` keyword | | Body | one or more statements | exactly one expression, after `=>` | | Separator between cases | none (new `case` line) | a comma; a trailing comma is allowed | | Default | `default:` or `case _:` | only `_ =>` | | Empty case | allowed; falls through to the next case | not allowed; every case needs a body | | Result | none | the value of the matching case | A few consequences of that table: - Because a case body must be an **expression**, you cannot put a sequence of statements inside a switch expression case. If a branch needs several steps, call a function from it or use a statement instead. - `throw` is an expression in Dart, so `_ => throw FormatException('Invalid')` is a legal last case. - To make several patterns share one body in an expression, use a **logical-or pattern** (`Plan.pro || Plan.team => ...`) rather than stacked empty cases. - In a Dart 3 switch **statement**, a non-empty case no longer falls through, so `break` at its end is redundant; that is statement behaviour, not something the expression form has at all. ## Where a switch expression can appear A switch expression may be written wherever Dart accepts an expression: - the right-hand side of a declaration or assignment: `final price = switch (plan) { ... };` - a `return` or an arrow body: `String label(Plan p) => switch (p) { ... };` - a function or constructor argument, including a Flutter widget argument such as `Text(switch (page) { ... })`; - inside a collection literal, string interpolation or another expression. The one exception is the **start of an expression statement**. A line that begins with `switch (` is parsed as a switch statement, so a bare `switch (x) { ... };` used only for its value is not a switch expression. If you want side effects and no value, write the statement form. ## Exhaustiveness: the rule that really differs 1. A **switch expression must always be exhaustive**: every value of the scrutinee's static type must match some case. If one could slip through, the analyzer reports `non_exhaustive_switch_expression`, a compile-time error. There is no "produce nothing" outcome for an expression. 2. A **switch statement** is checked the same way **only** when it switches over an *exhaustive type* — `bool`, an enum, a `sealed` class, or a nullable version of one of those. Then a missing case is the error `non_exhaustive_switch_statement`. 3. A switch statement over any other type — `int`, `String`, an ordinary class — may leave values unmatched; they simply run no case. 4. A `_` case (or `default` in a statement) matches everything and therefore makes any switch exhaustive — at the cost of hiding future omissions. ## The result type A switch expression has a static type inferred from its case bodies (their common upper bound, guided by the context type, such as the declared type of the variable it initialises). If one case returns `int` and another `double`, the result is `num`; if the context is `String`, every case must produce a `String`. ```dart enum Plan { free, pro, team } int monthlyPriceCents(Plan plan) => switch (plan) { Plan.free => 0, Plan.pro => 900, Plan.team => 2400, }; void logPlan(Plan plan) { switch (plan) { case Plan.free: print('free tier'); case Plan.pro || Plan.team: print('paid tier'); } } ``` ## Choosing between them - Reach for the **expression** when every branch computes a value of the same kind — a price, a label, a widget, an icon. - Reach for the **statement** when branches perform different side effects, need several statements, or should deliberately do nothing for unmatched values of an open type. - Prefer letting the compiler prove coverage over adding `_` by reflex: on enums and sealed types the explicit list of cases is what turns a new value into a compile error rather than a silent default. Switch expressions need a language version of at least 3.0; since Dart 3.10, dot shorthands also let cases on an enum be written as `.pro` when the type is known from the scrutinee.

  • Why can't a Dart switch expression begin an expression statement?
    A statement that starts with `switch (` is parsed as a switch statement, so the language reserves that position for the statement form. If you only want side effects, write the statement; if you want the value, put the expression somewhere that consumes it, such as an initializer, a `return` or an argument.
  • How do several patterns share one body in a Dart switch expression?
    Use a logical-or pattern: `Plan.pro || Plan.team => 'PAID'`. Stacking empty cases, which works in a switch statement, is not allowed in an expression because every case must have a body. A logical-or pattern can also share a single `when` guard.
  • What is the type of a Dart switch expression whose cases return int and double?
    Dart infers the result from the case bodies' common upper bound, guided by any context type, so an `int` case and a `double` case give `num`. If the context demands `String`, every case must produce a `String` or the code does not compile.

saying these in an interview costs you the question

  • Switch expressions allow a default: case like statements do.
  • A switch expression case can hold several statements in braces.
  • Only switch statements are checked for exhaustiveness.
  • A switch expression can be written anywhere a statement can, including alone on a line.
  • Switch expressions in Dart need a break after each case.
open as a page

In Dart 3, how would you model a PaymentResult as a sealed class hierarchy and handle every outcome with an exhaustive switch?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Declare sealed class PaymentResult with one subclass per outcome in the same library, each holding its own data, then switch over it with one object pattern per subtype and no wildcard. The compiler proves coverage and flags every switch when an outcome is added.

open as a page

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?

level: middleimportance: should knowfreq 46%

basics

~20 s

Every 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.

open as a page

In a Dart 3 switch, what happens when a case's when guard evaluates to false, and how do guarded cases affect exhaustiveness?

level: middleimportance: should knowfreq 38%

basics

~20 s

A 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.

open as a page