Watch the video (12 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Every switch Is a Pattern switch
  5. 2. Every Pattern Kind in One switch
  6. 3. Variables Bound in a Case
  7. 4. Guards with when
  8. 5. Which Switches Must Be Exhaustive?
  9. 6. Exhaustiveness Over Records
  10. 7. Shared Bodies with ||
  11. 8. Shared Bodies with Stacked Cases
  12. 9. Order and Unreachable Cases
  13. 10. Replacing if-else Chains with a Record switch
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 81 — Pattern Matching with switch

Category13 — Patterns & Modern Dart
Lecture081 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelIntermediate

You now know every kind of pattern, but mostly one at a time. A switch lets 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 from dart analyze, on Dart 3.12.2.


Prerequisites

Students should already understand, from Lectures 024–080:

  • switch statements 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 when guards, 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-else chain 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.

dart
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');
  }
}
output
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:

dart
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));
}
output
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
ArmPattern kind
null, 0constant
int ntyped variable
int() && > 100object && 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:

dart
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'.
}
output
Sara gets free delivery for 5 items

Each 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:

dart
void main() {
  var isMember = true;
  var total = 3000;
  // var fee = switch (isMember) {
  //   true when total >= 5000 => 0,
  //   false => 200,
  // }; // ✗
}
output
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:

dart
enum Weather { sunny, cloudy, rainy }

void main() {
  var today = Weather.rainy;
  // switch (today) {
  //   case Weather.sunny:
  //     print('Sunglasses');
  //   case Weather.cloudy:
  //     print('Jacket');
  // } // ✗
}
output
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:

dart
void main() {
  var status = (true, false);
  // var label = switch (status) {
  //   (true, true) => 'Done',
  //   (true, false) => 'Paid',
  //   (false, true) => 'Shipped',
  // }; // ✗
}
output
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:

dart
void main() {
  var status = (false, false);
  var label = switch (status) {
    (true, true) => 'Complete',
    (true, false) => 'Waiting to ship',
    (false, _) => 'Waiting for payment',
  };
  print(label);
}
output
Waiting for payment

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

dart
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);
}
output
On an axis, 7 from the origin

Whichever branch matches, d holds a value. Use different names and Dart refuses:

dart
void main() {
  var point = (3, 0);
  // switch (point) {
  //   case (int x, 0) || (0, int y):
  //     print('On an axis');
  //   default:
  //     print('Elsewhere');
  // } // ✗
}
output
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:

output
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:

dart
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');
  }
}
output
On an axis at 3

Rename one d to y and use d in the body:

output
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:

dart
void main() {
  var isRaining = true;
  var advice = switch (isRaining) {
    true => 'Umbrella',
    false => 'Sunglasses',
    _ => 'No idea', // never reached
  };
  print(advice);
}
output
Umbrella
output
warning - This case is covered by the previous cases. Try removing the case clause, or
restructuring the preceding patterns. - unreachable_switch_case

The 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:

dart
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));
}
output
Coat and umbrella
T-shirt

Put both values in a record and switch on it. Each arm reads like a row of a table:

dart
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));
}
output
Coat and umbrella
T-shirt

Note 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:

dart
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');
  }
}
output
large on Fri: Rs 457
small on Sat: Rs 300
medium on Sun: Rs 380

Enum 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

dart
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);
}
output
Right

Example 2 — List Shapes

dart
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);
}
output
From 70 to 85

The three arms cover lists of any length, so no _ is needed.

Example 3 — Map Pattern in a Statement

dart
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');
  }
}
output
Sara logged in

Example 4 — Object Pattern with a Guard

dart
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);
}
output
Only 2 Pen left

Example 5 — bool Pair Without _

dart
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);
}
output
Call an electrician

Example 6 — Shared Arm with ||

dart
void main() {
  var pair = ('tea', 'Ali');
  var greeting = switch (pair) {
    ('tea', var who) || (var who, 'tea') => '$who likes tea',
    _ => 'No tea here',
  };
  print(greeting);
}
output
Ali likes tea

Example 7 — Student Marks

dart
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);
}
output
Zayan: not allowed to sit

Example 8 — Shopping Total

dart
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');
}
output
Pay: 4680.0

Example 9 — Bank Balance

dart
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');
  }
}
output
Overdrawn: add money

Example 10 — Temperature and Rain

dart
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);
}
output
Go swimming

Common Beginner Mistakes

1. Single parentheses around two values

dart
void main() {
  var temp = 12;
  var isRaining = true;
  // var s = switch (temp, isRaining) { _ => 'x' }; // ✗
}
output
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 _ on bool, 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 run stays 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 _.

output
Price: Rs 500

Try it yourself before opening the solution.

Show solution
dart
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');
}
output
Price: Rs 500

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Store point = (0, 0) and use a switch expression to print Origin for (0, 0) and Elsewhere for anything else.
  2. Store items = ['pen']. Print Empty, One item: pen, or Many items with list patterns.
  3. Store pair = (true, true). Write a switch expression with no _ arm that prints Both for (true, true) and Not both otherwise.

Application

  1. Store user = {'name': 'Ali', 'age': 20}. Using a map pattern in a switch statement, print Ali is an adult when age is 18 or more, Ali is a minor otherwise.
  2. Store move = (0, 3). Using one || arm with variable d, print Straight move of 3 when one coordinate is 0, and Diagonal otherwise.
  3. Write String plan(bool isWeekend, int hour) as a switch on (isWeekend, hour): weekends give Sleep in, weekdays before 8 give Early start, other weekdays Off to work. Print plan(false, 9) and plan(true, 6).
  4. Using enum Level { low, mid, high }, write a switch statement on Level.mid that prints Medium. Then explain what happens if you delete the Level.high case.

Challenge

  1. Why does this not compile? Fix it so it prints Both lists start with 1:

    dart
    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);
    }
  2. Predict the output, then say which line dart analyze warns about:

    dart
    void main() {
      var code = 404;
      var text = switch (code) {
        200 => 'OK',
        >= 400 && < 500 => 'Client error',
        int n => 'Code $n',
        404 => 'Not found',
      };
      print(text);
    }
  3. Zayan's game scores a turn by (dice, isBonus): a 6 with bonus scores 20; any bonus roll scores the dice times 2; a 1 without bonus scores 0; otherwise the dice value. Store (4, true) and print Score: 8, binding the dice in a variable where needed.


Practice Answers

Question 1 — Solution

dart
void main() {
  var point = (0, 0);
  print(switch (point) {
    (0, 0) => 'Origin',
    _ => 'Elsewhere',
  });
}
output
Origin

Question 2 — Solution

dart
void main() {
  var items = ['pen'];
  print(switch (items) {
    [] => 'Empty',
    [var only] => 'One item: $only',
    _ => 'Many items',
  });
}
output
One item: pen

Question 3 — Solution

dart
void main() {
  var pair = (true, true);
  print(switch (pair) {
    (true, true) => 'Both',
    (false, _) || (_, false) => 'Not both',
  });
}
output
Both

Question 4 — Solution

dart
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');
  }
}
output
Ali is an adult

Question 5 — Solution

dart
void main() {
  var move = (0, 3);
  print(switch (move) {
    (var d, 0) || (0, var d) => 'Straight move of $d',
    _ => 'Diagonal',
  });
}
output
Straight move of 3

Question 6 — Solution

dart
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));
}
output
Off to work
Sleep in

Question 7 — Solution

dart
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');
  }
}
output
Medium

Without 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:

output
Both lists start with 1

Question 9 — Solution

output
Client error

404 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

dart
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');
}
output
Score: 8

Quick Revision

  1. What is the text after case called since Dart 3?
  2. Where can a variable bound by a case pattern be used?
  3. Why doesn't a guarded arm help exhaustiveness?
  4. Which switch statements must be exhaustive?
  5. What must be true of variables on both sides of ||?
  6. Why does switch ((a, b)) need two pairs of parentheses?

Key Takeaways

  • Every case and every arm holds a pattern; all pattern kinds work in both forms of switch.
  • Variables bound by a pattern live only in that case's guard and body.
  • A when guard 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-variable if chain, 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.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.