In Dart 3, how does a switch expression differ from a switch statement, and where may a switch expression appear?
answer
- a value versus control flow
- => bodies, commas, no case keyword
- only _ as the default
- never at the start of a statement
- the expression form must cover everything
basics
~20 sA 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 sA 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 linesenum 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
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.
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.
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.
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.