Dart (3.13) Module 3 – Control flow and functions
Dart switch statements and expressions
Write Dart 3 switch statements with no fall-through, match with relational, logical-or and type-test patterns, return a value, and fix exhaustiveness errors.
What you will learn
- Write Dart 3 switch statements without fall-through mistakes
- Match with relational, logical-or, type-test and when-guard patterns in cases
- Build a switch expression that returns a value and covers every case
- Recognise exhaustiveness errors and fix them
Before you start
What you’ll learn · Before you start
What you will learn
- Write Dart 3 switch statements without fall-through mistakes
- Match with relational, logical-or, type-test and when-guard patterns in cases
- Build a switch expression that returns a value and covers every case
- Recognise exhaustiveness errors and fix them
Before you start
On this page
A switch compares one value against many cases, and Dart 3 rebuilt it. A case no longer falls through to the next, so
the break that older tutorials put everywhere is gone; cases are now patterns that can test ranges and types; and a
switch can be an expression that returns a value the compiler forces you to cover completely.
You will run a switch that proves there is no fall-through, match with relational, logical-or, type-test and when-guard patterns, turn marks into a grade with a switch expression, and read the compile error Dart gives when a switch expression misses a case.
A case ends by itself
nofall.dart switches on the number 2 with cases 1, 2 and 3, then shows how empty cases share a body:
// In Dart 3 a non-empty case ends by itself: no break, and no fall-through.
// An empty case shares the body of the next case.
void main() {
const code = 2;
switch (code) {
case 1:
print('one');
case 2:
print('two');
case 3:
print('three');
default:
print('many');
}
for (final day in ['Sat', 'Sun', 'Mon']) {
switch (day) {
case 'Sat':
case 'Sun':
print('$day: weekend');
default:
print('$day: weekday');
}
}
} Output
two Sat: weekend Sun: weekend Mon: weekday
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run nofall.dart
The first switch printed only two. In Dart 3 a non-empty case ends on its own, so matching case 2 ran its print and
then left the switch, without reaching case 3. The second switch printed Sat: weekend and Sun: weekend: an empty
case, like case 'Sat': with no body, shares the body of the next case, which is how you group values. The history
behind the change is worth a look:
- 2.x
break at every case old rule
A non-empty case had to end with break, continue, return or throw, or the program did not compile. This is why tutorials from that era put break after every case.
- 3.0
Patterns and no fall-through new syntax
A non-empty case now ends by itself. Cases became patterns (constants, logical-or, relational, type tests, when guards), and switch expressions returned a value that must be exhaustive.
- 3.10
Dot shorthands in cases shorthand
An enum case can drop the type name, writing case .debug instead of case Level.debug, when the type is already known. The enums lesson covers it.
The timeline has 3 events, from 2.x to 3.10.
Up to Dart 2 every non-empty case had to end with break, continue, return or throw, which is why older code is
full of break. Removing fall-through also removed a classic bug: a case that forgot its break and silently ran the
next one.
Checkpoint
Check: A case ends by itself
Cases are patterns
A case label is a pattern: A description of a value's shape that the value is matched against. It can test the value and bind parts of it to variables, and Dart uses patterns in switch cases, if-case statements and destructuring declarations., not just a constant. patterns.dart matches
an Object against a type test, a when guard, and a logical-or of two constants:
// Cases are patterns: a type test (int n), a when guard, a logical-or of two
// constants, and another type test that binds the matched value.
String describe(Object v) => switch (v) {
int n when n < 0 => 'negative',
int n => 'int $n',
'Dart' || 'Flutter' => 'a Google language',
String s => 'string (${s.length})',
_ => 'other',
};
void main() {
for (final v in [-3, 7, 'Dart', 'hello', 3.14]) {
print('$v -> ${describe(v)}');
}
} Output
-3 -> negative 7 -> int 7 Dart -> a Google language hello -> string (5) 3.14 -> other
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run patterns.dart
The output shows each kind at work: -3 -> negative because int n when n < 0 matched first, 7 -> int 7 because the
plain int n pattern bound n to 7, Dart -> a Google language from the logical-or pattern 'Dart' || 'Flutter',
and hello -> string (5) from the String s pattern. Order still decides the winner, so the guarded case must sit
above the plain int n case or no negative number would ever reach it. Combined relational patterns describe ranges;
status.dart turns an HTTP code into its class from RFC 9110:
// HTTP status classes from RFC 9110, section 15, with combined relational
// patterns. An out-of-range code throws, so every path returns a known class.
String statusClass(int code) => switch (code) {
>= 100 && < 200 => 'informational',
>= 200 && < 300 => 'success',
>= 300 && < 400 => 'redirection',
>= 400 && < 500 => 'client error',
>= 500 && < 600 => 'server error',
_ => throw ArgumentError.value(code, 'code', 'not a valid HTTP status'),
};
void main() {
for (final c in [100, 204, 301, 404, 503]) {
print('$c ${statusClass(c)}');
}
} Output
100 informational 204 success 301 redirection 404 client error 503 server error
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run status.dart
Each case such as >= 200 && < 300 is one pattern that tests both ends, so 204 printed success and 404 printed
client error. The final _ case throws for anything outside 100 to 599, so the function never returns an unknown
class.
The plainest pattern is a constant, the kind every Dart 2 switch already used: case 1: matches the value 1, and a
bare name such as case maxRetries: matches against that constant’s value. Everything else is new in Dart 3 and builds
on it. A type-test pattern like int n matches when the value has that type and binds a variable you can read in the
body; a when guard adds any boolean test to a case; a logical-or joins alternatives; and relational patterns compare
with >=, >, <= and <. Patterns also destructure lists, maps and records, which the patterns module covers in
full. This table lines up the kinds used here:
| Pattern | Example case | Matches |
|---|---|---|
| constant | case 404: |
the value equal to the constant |
| logical-or | case 'Dart' || 'Flutter': |
either alternative |
| relational | case >= 200 && < 300: |
a value in the range |
| type test | case int n: |
a value of that type, bound to n |
| guarded | case int n when n < 0: |
a match whose when test is also true |
Checkpoint
Check: Cases are patterns
switch expressions return a value
A switch statement runs code; a switch expression: A switch written as an expression, with comma-separated cases and => instead of a colon. It produces a value you can assign or return, and its cases must be exhaustive. produces a value. grade.dart turns marks into a letter and assigns the result:
// A switch expression returns a value. Relational patterns test ranges, and
// the first matching case wins, so the order runs from the highest band down.
String grade(int marks) => switch (marks) {
>= 90 => 'A',
>= 75 => 'B',
>= 35 => 'C',
_ => 'F',
};
void main() {
for (final m in [74, 75, 89, 90]) {
print('$m -> ${grade(m)}');
}
} Output
74 -> C 75 -> B 89 -> B 90 -> A
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run grade.dart
It printed 74 -> C, 75 -> B, 89 -> B and 90 -> A. The syntax differs from the statement: cases are separated by
commas, each uses => instead of a colon, and there is no case keyword. Because the cases are tried top to bottom
and the first match wins, the bands run from the highest down; >= 75 would swallow every mark of 90 or more if it
came first.
Being an expression is what makes it fit anywhere a value belongs: the right-hand side of an assignment, an argument,
the body of an arrow function, or an element inside a list literal. A bare _ stands for “anything else” and plays the
part default plays in a statement, so _ => 'F' catches every mark the bands above it did not. Step through one
call:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The value to match. The switch expression starts with marks set to 74 and tries its cases from the top. The first case whose pattern matches gives the whole expression its value.
- First case. The relational pattern >= 90 asks whether 74 is at least 90. It is not, so this case does not match and the search moves on.
- Second case. The next pattern, >= 75, asks whether 74 is at least 75. It is not, so this case is skipped too.
- Third case matches. The pattern >= 35 matches, because 74 is at least 35, so the expression takes the value 'C'. The wildcard case below is never tried, which is why grade(74) printed 74 -> C.
Checkpoint
Check: switch expressions return a value
Exhaustiveness is checked at compile time
A switch expression must handle every possible value, or it does not compile. nonexhaustive.dart switches on a bool
but covers only true:
// A switch expression must be exhaustive. Over a bool, that means both true
// and false; this one covers only true, so it does not compile.
String label(bool on) => switch (on) {
true => 'on',
};
void main() => print(label(true)); Output (exit status 254)
Printed as an error (standard error)
nonexhaustive.dart:3:34: Error: The type 'bool' is not exhaustively matched by the switch cases since it doesn't match 'false'.
Try adding a wildcard pattern or cases that match 'false'.
String label(bool on) => switch (on) {
^
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run nonexhaustive.dart
The compiler refused it, reporting that the bool type “is not exhaustively matched” because the false case is
missing, and the status was 254, which Dart uses for code that does not compile. exhaustiveness: The compiler's check that a switch expression's cases cover every possible value of the matched type; for a bool, enum or sealed type a missing case is a compile-time error.
is checked for types with a known, closed set of values: bool, enums and sealed classes. The fix is to cover the
missing case, or add a _ wildcard for everything else. exhaustive.dart adds false:
// Covering both true and false makes the switch expression exhaustive.
String label(bool on) => switch (on) {
true => 'on',
false => 'off',
};
void main() {
print(label(true));
print(label(false));
} Output
on off
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run exhaustive.dart
It printed on and off. Covering cases by name rather than with _ is what makes exhaustiveness useful: add a value
to an enum later, and every switch that named the old values, but not a _, becomes a compile error pointing you at
the code to update.
This is why an enum or a sealed class pays off in a switch. Picture an OrderState with placed, packed and
shipped: a switch expression that names all three is accepted, but the day someone adds cancelled, every such
switch that has no _ stops compiling until it handles the new state. The compiler has turned “did we update every
screen?” from a question you have to remember into a build error you cannot miss. Reach for a _ only when the type is
open, such as int or String, where no finite list of cases can ever be complete, or when a genuine catch-all is the
behaviour you want.
One-branch forms
When you need to match just one pattern, if (value case [first, ...]) is the one-branch form of a switch, and
case .debug: drops a known enum’s type name. Both come from the patterns and enums modules.
Checkpoint
Check: Exhaustiveness is checked
Practise: two switch expressions
Write both functions as switch expressions, not if/else chains.
String statusClass(int code)returns the five HTTP classes from RFC 9110 and throwsArgumentErrorfor a code outside 100 to 599; match it against the boundaries 100, 199, 200, 404 and 599.String grade(int marks)returns'A'for 90 and above,'B'for 75 to 89,'C'for 35 to 74 and'F'below 35; check 34, 35, 74, 75, 89 and 90.- Run both and confirm the boundaries fall on the side you intended; an off-by-one here is the usual grading bug.
- Remove the last case from one function and read the error: a switch expression over
intneeds a_, because no finite list ofintvalues is exhaustive.
When the boundaries hold, the self-check revisits every section:
Practice that adapts to you
Practise this lesson
5 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): In Dart 3, do you still need break at the end of a switch case?
No. Since Dart 3 a non-empty case ends by itself, so execution leaves the switch after the matching case runs; nofall.dart
switched on 2 and printed only two, never reaching case 3. You write break only to leave a case early, the same way
you would inside a loop. The one place a label still appears is a deliberate jump with continue to a labelled case,
which replaces the old fall-through when you really do want one case to run another’s body. Empty cases, with no body
at all, still share the next case’s body, and that is how you group several values without any break. Code that puts
break after every case is almost always ported from Dart 2.
For deeper practice, the bank carries two questions that take switch into real design decisions:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
An order pipeline maps an OrderState enum to a screen with a switch that ends in a default returning an error page. A new state, refundPending, is added for a feature, the code compiles, and refunds silently show the error page in production. Explain why the compiler did not help, and how you would structure the switch so it would.
-
Fresher to mid
A grading function written as a switch expression with relational patterns gives every student a C, even those who scored 95. The bands are all present and the code compiles. What is the likely cause, and how do you find and fix it?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Does a Dart 3 switch case need break at its end?
No. A non-empty case ends by itself, so there is no fall-through; an empty case shares the body of the next case. break is only for leaving a case early.
-
Which patterns can a Dart 3 case use?
Constants, logical-or ('Dart' || 'Flutter'), relational (>= 200 && < 300), type tests (int n) and when guards. Cases are tried top to bottom, and the first match wins.
-
How is a switch expression written, and what does it give?
It produces a value: each case is a pattern, =>, then the result, with commas between the cases. Since the first match wins, order bands from the most specific down.
-
What happens when a switch expression over an enum misses a value?
It is a compile error: a switch expression must be exhaustive, and over a bool, an enum or a sealed type the compiler knows every value. Adding an enum value then points you at every switch that needs a case.
-
Why name every case of a closed type rather than end with _?
A catch-all _ also matches values added later, so a new enum value would silently take its branch. Named cases turn that into a compile error you have to answer.
-
What is the one-branch form of a switch?
if (value case pattern), such as if (value case [first, ...]): it runs its block when the one pattern matches.
Key takeaways
- In Dart 3 a non-empty case ends by itself, so there is no fall-through and no need for
break; an empty case shares the body of the next case. - Case labels are patterns: constants, logical-or (
'Dart' || 'Flutter'), relational (>= 200 && < 300), type tests (int n) andwhenguards, tried top to bottom with the first match winning. - A switch expression produces a value, with
=>and comma-separated cases, so order the bands from the most specific down. - A switch expression must be exhaustive; over a
bool,enumor sealed type a missing case is a compile error, which is what makes the compiler point you at every switch when you add an enum value. - Prefer naming cases over a catch-all
_for closed types, so a later value turns silent fall-through into a checked compile error.
Terms in this lesson
- exhaustiveness
- The compiler's check that a switch expression's cases cover every possible value of the matched type; for a bool, enum or sealed type a missing case is a compile-time error.
- pattern
- A description of a value's shape that the value is matched against. It can test the value and bind parts of it to variables, and Dart uses patterns in switch cases, if-case statements and destructuring declarations.
- switch expression
- A switch written as an expression, with comma-separated cases and => instead of a colon. It produces a value you can assign or return, and its cases must be exhaustive.
References
- Branches (The Dart project (Google))
- Patterns (The Dart project (Google))
- Pattern types (The Dart project (Google))
- RFC 9110: HTTP Semantics, section 15 (IETF)
Report a problem with this lesson
Kept only in this browser. Your Learn progress