On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Every switch Is a Pattern switch
- 2. Every Pattern Kind in One switch
- 3. Variables Bound in a Case
- 4. Guards with when
- 5. Which Switches Must Be Exhaustive?
- 6. Exhaustiveness Over Records
- 7. Shared Bodies with ||
- 8. Shared Bodies with Stacked Cases
- 9. Order and Unreachable Cases
- 10. Replacing if-else Chains with a Record switch
- 11. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 81 — Pattern Matching with switch
| Category | 13 — Patterns & Modern Dart |
| Lecture | 081 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
You now know every kind of pattern, but mostly one at a time. A
switchlets you line them up, test a value against each in turn, and have Dart check that nothing was forgotten.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every error and warning in this lecture is real. Errors come from
dart run, warnings fromdart analyze, on Dart 3.12.2.
Prerequisites
Students should already understand, from Lectures 024–080:
switchstatements and switch expressions,_,||, relational patterns,when(Lectures 024–025)- functions, lists, maps, and simple classes (Lectures 030–053)
- enums and exhaustive switches on them (Lecture 067)
- records (Lecture 076)
- every pattern kind: destructuring, variable, list, record, object, and map patterns (Lectures 077–080)
Connection With Previous Lectures
In Lectures 077–080 we used patterns mostly to take values apart, when we already knew the
value would fit. Real programs are less certain: a value might be a list or a map, and a record
might hold any mix of true and false. Testing each shape with if and is gets long, and
nothing warns us when we forget a combination. A pattern switch fixes both.
Learning Objectives
By the end of this lecture you will be able to:
- Use any pattern kind in a switch statement and a switch expression
- Bind variables in a case and use them in its guard and body
- Add
whenguards, and explain why they don't count for exhaustiveness - Recognize which switches must be exhaustive
- Share one body with
||or stacked cases, following the variable rules - Spot unreachable cases from the analyzer's warning
- Replace an
if-elsechain with a switch on a record such as(temp, isRaining)
1. Every switch Is a Pattern switch
Since Dart 3, what follows case (in a statement) or comes before => (in an expression) is a
pattern. The values 1, 'red' and >= 90 from Lectures 024–025 were simple patterns
all along.
Analogy — a switch is a row of sorting slots. Each slot has a shape cut into it, and the value drops into the first slot it fits.
void main() {
var point = (0, 5);
switch (point) {
case (0, 0):
print('Origin');
case (0, var y):
print('On the y-axis at $y');
default:
print('Somewhere else');
}
}On the y-axis at 5(0, var y) needs a first field of 0 and catches the second in y.
2. Every Pattern Kind in One switch
All the patterns from this category work as cases. One switch expression can test a value of unknown type against all of them:
String describe(Object? value) => switch (value) {
null => 'nothing',
0 => 'zero',
int n when n < 0 => 'negative number $n',
int() && > 100 => 'big number',
int n => 'number $n',
String(isEmpty: true) => 'empty text',
String s => 'text "$s"',
[] => 'empty list',
[var first, ...] => 'list starting with $first',
{'city': String city} => 'map with city $city',
(var a, var b) => 'pair of $a and $b',
_ => 'something else',
};
void main() {
for (var v in [null, 0, -4, 250, 7, '', 'Ali', []]) {
print(describe(v));
}
print(describe([3, 4]));
print(describe({'city': 'Lahore'}));
print(describe((1, 2)));
print(describe(2.5));
}nothing
zero
negative number -4
big number
number 7
empty text
text "Ali"
empty list
list starting with 3
map with city Lahore
pair of 1 and 2
something else| Arm | Pattern kind |
|---|---|
null, 0 | constant |
int n | typed variable |
int() && > 100 | object && relational |
String(isEmpty: true) | object pattern checking a getter |
[], [var first, ...] | list |
{'city': String city} | map |
(var a, var b) | record |
_ | wildcard |
2.5 is a double, so only _ fits.
3. Variables Bound in a Case
A variable created by a pattern exists only inside its own case — in the guard and the body — and nowhere else:
void main() {
var order = ('Sara', 5);
switch (order) {
case (var name, var qty) when qty > 3:
print('$name gets free delivery for $qty items');
case (var name, _):
print('$name pays delivery');
}
// print(qty); // ✗ Error: Undefined name 'qty'.
}Sara gets free delivery for 5 itemsEach case has its own scope, so both can bind name.
4. Guards with when
A guard is checked after the pattern matches, so it can use the pattern's variables, as
qty > 3 did in section 3. If the guard is false, Dart moves on to the next case.
Guards don't count for exhaustiveness. Dart cannot know in advance whether total >= 5000
will be true, so a guarded arm covers nothing for sure:
void main() {
var isMember = true;
var total = 3000;
// var fee = switch (isMember) {
// true when total >= 5000 => 0,
// false => 200,
// }; // ✗
}Error: The type 'bool' is not exhaustively matched by the switch cases since it doesn't match 'true'.
Try adding a wildcard pattern or cases that match 'true'.Add a plain true => 100 arm after the guarded one.
5. Which Switches Must Be Exhaustive?
A switch expression must always be exhaustive. A switch statement must be exhaustive
too when it switches on a type with a fixed set of values: bool, an enum, or a record made
of those. Forget an enum value in a statement and the program does not compile:
enum Weather { sunny, cloudy, rainy }
void main() {
var today = Weather.rainy;
// switch (today) {
// case Weather.sunny:
// print('Sunglasses');
// case Weather.cloudy:
// print('Jacket');
// } // ✗
}Error: The type 'Weather' is not exhaustively matched by the switch cases since it doesn't match 'Weather.rainy'.
Try adding a default case or cases that match 'Weather.rainy'.A statement on an int or a String may leave values uncovered — with no match, nothing
runs, as in Lecture 024. Exhaustiveness over class hierarchies needs one more tool: sealed
classes, in Lecture 082.
6. Exhaustiveness Over Records
Dart checks every combination of a record's fields. A (bool, bool) has four, and Dart
names the one you missed:
void main() {
var status = (true, false);
// var label = switch (status) {
// (true, true) => 'Done',
// (true, false) => 'Paid',
// (false, true) => 'Shipped',
// }; // ✗
}Error: The type '(bool, bool)' is not exhaustively matched by the switch cases since it doesn't match '(false, false)'.
Try adding a wildcard pattern or cases that match '(false, false)'.A wildcard inside the record covers several combinations at once:
void main() {
var status = (false, false);
var label = switch (status) {
(true, true) => 'Complete',
(true, false) => 'Waiting to ship',
(false, _) => 'Waiting for payment',
};
print(label);
}Waiting for payment7. Shared Bodies with ||
|| lets two patterns share one arm. If the arm uses a variable, every branch must bind
it, with the same name and the same type:
void main() {
var point = (0, 7);
var place = switch (point) {
(var d, 0) || (0, var d) => 'On an axis, $d from the origin',
_ => 'Off the axes',
};
print(place);
}On an axis, 7 from the originWhichever branch matches, d holds a value. Use different names and Dart refuses:
void main() {
var point = (3, 0);
// switch (point) {
// case (int x, 0) || (0, int y):
// print('On an axis');
// default:
// print('Elsewhere');
// } // ✗
}Error: Variable pattern 'y' is missing in this branch of the logical-or pattern.
Try declaring this variable pattern in the branch.
Error: Variable pattern 'x' is missing in this branch of the logical-or pattern.
Try declaring this variable pattern in the branch.Same name with different types — case int n || String n: — fails too:
Error: Variable pattern 'n' doesn't have the same type or finality in all cases.8. Shared Bodies with Stacked Cases
In a statement, empty cases fall through to the next body (Lecture 024). The same rule applies — a variable the body uses must come from every stacked case:
void main() {
var point = (3, 0);
switch (point) {
case (int d, 0):
case (0, int d):
print('On an axis at $d');
default:
print('Elsewhere');
}
}On an axis at 3Rename one d to y and use d in the body:
Error: The variable 'd' is available in some, but not all cases that share this body.9. Order and Unreachable Cases
Dart tries cases top to bottom and takes the first match. A case placed after one that
already catches everything can never run, and dart analyze warns you:
void main() {
var isRaining = true;
var advice = switch (isRaining) {
true => 'Umbrella',
false => 'Sunglasses',
_ => 'No idea', // never reached
};
print(advice);
}Umbrellawarning - This case is covered by the previous cases. Try removing the case clause, or
restructuring the preceding patterns. - unreachable_switch_caseThe program still runs, but the line is useless. The analyzer only flags cases it can
prove unreachable: >= 50 before >= 90 (Lecture 025) gets no warning, so ordering
ranges is still your job.
10. Replacing if-else Chains with a Record switch
When a decision depends on two values, an if chain repeats both in every condition:
String advice(int temp, bool isRaining) {
if (isRaining && temp < 15) {
return 'Coat and umbrella';
} else if (isRaining) {
return 'Umbrella';
} else if (temp < 15) {
return 'Coat';
} else {
return 'T-shirt';
}
}
void main() {
print(advice(12, true));
print(advice(20, false));
}Coat and umbrella
T-shirtPut both values in a record and switch on it. Each arm reads like a row of a table:
String advice(int temp, bool isRaining) => switch ((temp, isRaining)) {
(< 15, true) => 'Coat and umbrella',
(_, true) => 'Umbrella',
(< 15, false) => 'Coat',
(_, false) => 'T-shirt',
};
void main() {
print(advice(12, true));
print(advice(20, false));
}Coat and umbrella
T-shirtNote the double parentheses: one pair for switch, one to build the record. And since
(_, true) and (_, false) cover everything, Dart proves the switch complete — an if
chain gets no such check.
11. A Real-World Example
Ali's café app prices a drink from its size, an optional member card, and the day:
enum Size { small, medium, large }
void main() {
var orders = [
(Size.large, 'GOLD-17', 'Fri'),
(Size.small, null, 'Sat'),
(Size.medium, 'SILVER-3', 'Sun'),
];
for (var order in orders) {
var price = switch (order) {
(Size.small, _, _) => 300,
(Size.medium, String card, _) when card.startsWith('GOLD') => 350,
(Size.medium, _, 'Sat' || 'Sun') => 380,
(Size.medium, _, _) => 400,
(Size.large, var card?, var day) when day != 'Sun' => 450 + card.length,
(Size.large, _, _) => 500,
};
print('${order.$1.name} on ${order.$3}: Rs $price');
}
}large on Fri: Rs 457
small on Sat: Rs 300
medium on Sun: Rs 380Enum constants, a guard, || inside a field, a null-check pattern card?, and wildcards —
and Dart has checked that all three sizes are covered.
Code Examples
Example 1 — Record Constants
void main() {
var move = (1, 0);
var direction = switch (move) {
(1, 0) => 'Right',
(-1, 0) => 'Left',
(0, 1) => 'Up',
(0, -1) => 'Down',
_ => 'Diagonal',
};
print(direction);
}RightExample 2 — List Shapes
void main() {
var scores = [70, 85];
var summary = switch (scores) {
[] => 'No scores',
[var only] => 'One score: $only',
[var first, ..., var last] => 'From $first to $last',
};
print(summary);
}From 70 to 85The three arms cover lists of any length, so no _ is needed.
Example 3 — Map Pattern in a Statement
void main() {
var json = {'type': 'login', 'user': 'Sara'};
switch (json) {
case {'type': 'login', 'user': String user}:
print('$user logged in');
case {'type': 'logout'}:
print('Someone logged out');
default:
print('Unknown event');
}
}Sara logged inExample 4 — Object Pattern with a Guard
class Product {
final String name;
final int stock;
Product(this.name, this.stock);
}
void main() {
var item = Product('Pen', 2);
var label = switch (item) {
Product(stock: 0) => 'Sold out',
Product(:var name, :var stock) when stock < 5 => 'Only $stock $name left',
Product(:var name) => '$name in stock',
};
print(label);
}Only 2 Pen leftExample 5 — bool Pair Without _
void main() {
var light = (isOn: false, isBroken: true);
var state = switch (light) {
(isOn: _, isBroken: true) => 'Call an electrician',
(isOn: true, isBroken: false) => 'Shining',
(isOn: false, isBroken: false) => 'Off',
};
print(state);
}Call an electricianExample 6 — Shared Arm with ||
void main() {
var pair = ('tea', 'Ali');
var greeting = switch (pair) {
('tea', var who) || (var who, 'tea') => '$who likes tea',
_ => 'No tea here',
};
print(greeting);
}Ali likes teaExample 7 — Student Marks
void main() {
var result = ('Zayan', 74, 60); // name, marks, attendance
var verdict = switch (result) {
(var n, _, < 75) => '$n: not allowed to sit',
(var n, >= 80, _) => '$n: Distinction',
(var n, >= 50, _) => '$n: Pass',
(var n, _, _) => '$n: Fail',
};
print(verdict);
}Zayan: not allowed to sitExample 8 — Shopping Total
void main() {
var cart = (5200, 'SAVE10'); // total, coupon
var finalPrice = switch (cart) {
(var t, 'SAVE10') when t >= 5000 => t * 0.9,
(var t, _) => t.toDouble(),
};
print('Pay: $finalPrice');
}Pay: 4680.0Example 9 — Bank Balance
void main() {
var account = (-200.0, false); // balance, isFrozen
switch (account) {
case (_, true):
print('Account frozen');
case (< 0, false):
print('Overdrawn: add money');
case (var b, false):
print('Balance: $b');
}
}Overdrawn: add moneyExample 10 — Temperature and Rain
void main() {
var celsius = 31;
var isRaining = false;
var plan = switch ((celsius, isRaining)) {
(_, true) => 'Stay in and read',
(>= 30, false) => 'Go swimming',
(>= 15, false) => 'Go to the park',
(_, false) => 'Go for a brisk walk',
};
print(plan);
}Go swimmingCommon Beginner Mistakes
1. Single parentheses around two values
void main() {
var temp = 12;
var isRaining = true;
// var s = switch (temp, isRaining) { _ => 'x' }; // ✗
}Error: Expected ')' before this.Write switch ((temp, isRaining)).
2. Expecting a guarded arm to complete the switch
A when arm covers nothing for exhaustiveness (section 4). Follow it with an unguarded arm.
3. Different variables on the two sides of || — (int x, 0) || (0, int y) is an
error (section 7). Use one name in both branches, or none.
4. Using a case variable after the switch — it gives Error: Undefined name 'qty'.
(section 3). Store what you need in a variable declared before the switch.
5. Ignoring "This case is covered by the previous cases" — that case can never run. Remove it or move it up.
Best Practices
- Prefer exhaustive switches without
_onbool, enums, and records of them, so Dart reports the case you forget when a new value is added. - Put specific cases (constants, guards) above general ones (
var x,_). - Use
||or stacked cases only when every branch binds the same variables. - Switch on a record when a decision depends on two or three values.
- Run
dart analyze—dart runstays silent about unreachable cases.
Mini Challenge
Sara's parcel service prices by (weightKg, isExpress): express is 1000; otherwise under
1 kg is 250, under 5 kg 500, heavier 900. Price (3.5, false) without a top-level _.
Price: Rs 500Try it yourself before opening the solution.
Show solution
void main() {
var parcel = (3.5, false);
var price = switch (parcel) {
(_, true) => 1000,
(< 1, false) => 250,
(< 5, false) => 500,
(_, false) => 900,
};
print('Price: Rs $price');
}Price: Rs 500Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Store
point = (0, 0)and use a switch expression to printOriginfor(0, 0)andElsewherefor anything else. - Store
items = ['pen']. PrintEmpty,One item: pen, orMany itemswith list patterns. - Store
pair = (true, true). Write a switch expression with no_arm that printsBothfor(true, true)andNot bothotherwise.
Application
- Store
user = {'name': 'Ali', 'age': 20}. Using a map pattern in a switch statement, printAli is an adultwhen age is18or more,Ali is a minorotherwise. - Store
move = (0, 3). Using one||arm with variabled, printStraight move of 3when one coordinate is0, andDiagonalotherwise. - Write
String plan(bool isWeekend, int hour)as a switch on(isWeekend, hour): weekends giveSleep in, weekdays before8giveEarly start, other weekdaysOff to work. Printplan(false, 9)andplan(true, 6). - Using
enum Level { low, mid, high }, write a switch statement onLevel.midthat printsMedium. Then explain what happens if you delete theLevel.highcase.
Challenge
-
Why does this not compile? Fix it so it prints
Both lists start with 1:void main() { var lists = ([1, 2], [1, 5]); switch (lists) { case ([var a, ...], [var b, ...]) when a == b: print('Both lists start with $a'); default: print('Different'); } print(a); } -
Predict the output, then say which line
dart analyzewarns about:void main() { var code = 404; var text = switch (code) { 200 => 'OK', >= 400 && < 500 => 'Client error', int n => 'Code $n', 404 => 'Not found', }; print(text); } -
Zayan's game scores a turn by
(dice, isBonus): a6with bonus scores20; any bonus roll scores the dice times2; a1without bonus scores0; otherwise the dice value. Store(4, true)and printScore: 8, binding the dice in a variable where needed.
Practice Answers
Question 1 — Solution
void main() {
var point = (0, 0);
print(switch (point) {
(0, 0) => 'Origin',
_ => 'Elsewhere',
});
}OriginQuestion 2 — Solution
void main() {
var items = ['pen'];
print(switch (items) {
[] => 'Empty',
[var only] => 'One item: $only',
_ => 'Many items',
});
}One item: penQuestion 3 — Solution
void main() {
var pair = (true, true);
print(switch (pair) {
(true, true) => 'Both',
(false, _) || (_, false) => 'Not both',
});
}BothQuestion 4 — Solution
void main() {
var user = {'name': 'Ali', 'age': 20};
switch (user) {
case {'name': String n, 'age': int a} when a >= 18:
print('$n is an adult');
case {'name': String n}:
print('$n is a minor');
}
}Ali is an adultQuestion 5 — Solution
void main() {
var move = (0, 3);
print(switch (move) {
(var d, 0) || (0, var d) => 'Straight move of $d',
_ => 'Diagonal',
});
}Straight move of 3Question 6 — Solution
String plan(bool isWeekend, int hour) => switch ((isWeekend, hour)) {
(true, _) => 'Sleep in',
(false, < 8) => 'Early start',
(false, _) => 'Off to work',
};
void main() {
print(plan(false, 9));
print(plan(true, 6));
}Off to work
Sleep inQuestion 7 — Solution
enum Level { low, mid, high }
void main() {
var level = Level.mid;
switch (level) {
case Level.low:
print('Low');
case Level.mid:
print('Medium');
case Level.high:
print('High');
}
}MediumWithout the Level.high case it fails to compile: a statement on an enum must be exhaustive,
and the error names Level.high.
Question 8 — Solution
a exists only inside its case, so print(a) gives Error: Undefined name 'a'. Delete that
line and the program prints:
Both lists start with 1Question 9 — Solution
Client error404 matches >= 400 && < 500 first. dart analyze warns about the 404 arm: int n
already catches every int, so it is covered by the previous cases.
Question 10 — Solution
void main() {
var turn = (4, true);
var score = switch (turn) {
(6, true) => 20,
(var d, true) => d * 2,
(1, false) => 0,
(var d, false) => d,
};
print('Score: $score');
}Score: 8Quick Revision
- What is the text after
casecalled since Dart 3? - Where can a variable bound by a case pattern be used?
- Why doesn't a guarded arm help exhaustiveness?
- Which switch statements must be exhaustive?
- What must be true of variables on both sides of
||? - Why does
switch ((a, b))need two pairs of parentheses?
Key Takeaways
- Every
caseand every arm holds a pattern; all pattern kinds work in both forms ofswitch. - Variables bound by a pattern live only in that case's guard and body.
- A
whenguard runs after the pattern matches and never counts toward exhaustiveness. - Switch expressions are always exhaustive; switch statements are too on
bool, enums, and records of them. Dart names the missing value or combination. ||and stacked cases share a body only if every branch binds the same variables with the same types.- Cases are tried top to bottom; the analyzer flags cases it can prove unreachable.
- A switch on a record like
(temp, isRaining)replaces a two-variableifchain, with a completeness check.
What's Next?
Lecture 082 — Sealed Classes. Exhaustiveness works for bool, enums, and records — but a
switch over Object or over subclasses of an ordinary class still needs _, because anyone
could add a new subclass. Next, sealed closes a class family so Dart knows every subtype and
can check a switch over them, just as it does for enums.