Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 10 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

nofall.dart: no fall-through, and empty cases that share Dart · nofall.dart
// 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:

Timeline · 3 events in order How switch changed from Dart 2 to Dart 3 Walk the versions in order to see why older code looks different.
  1. 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.

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

  1. Question 1 of 2 A Dart 3 switch on 2 has non-empty cases 1, 2 and 3. What prints?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only the body of case 2

    nofall.dart printed only two. In Dart 3 a non-empty case does not fall through to the next.

  2. Question 2 of 2 What does an empty case, case 'Sat': with no body, do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It shares the body of the next case

    nofall.dart grouped 'Sat' and 'Sun' with an empty case, so both printed weekend.

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:

patterns.dart: type tests, a when guard and logical-or Dart · patterns.dart
// 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:

status.dart: HTTP status classes with relational patterns Dart · status.dart
// 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

  1. Question 1 of 2 What does the case 'Dart' || 'Flutter' match?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Either the string 'Dart' or the string 'Flutter'

    patterns.dart matched 'Dart' with the logical-or case and printed a Google language.

  2. Question 2 of 2 In describe, int n when n < 0 sits above int n. What matches -3?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: int n when n < 0, so it prints negative

    patterns.dart printed -3 -> negative. A guarded case must sit above the plainer case or it is never reached.

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:

grade.dart: marks to a grade with a switch expression Dart · grade.dart
// 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:

Interactive · step by step A switch expression picks the first matching case Step through grade(74). The cases are tried top to bottom, and the first match decides the value.
A switch expression picks the first matching case, step 4 of 4. Showing marks = 74, >= 90 => 'A', >= 75 => 'B', >= 35 => 'C' (matched), _ => 'F'. In focus: >= 35 => 'C'.marks = 74>= 90 => 'A'>= 75 => 'B'>= 35 => 'C'matched_ => 'F'

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. Second case. The next pattern, >= 75, asks whether 74 is at least 75. It is not, so this case is skipped too.
  4. 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

  1. Question 1 of 2 How does a switch expression differ from a switch statement?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It produces a value you can assign or return

    grade.dart assigned the result of a switch expression; a switch statement has no value to assign.

  2. Question 2 of 2 In grade, why must >= 90 come before >= 75?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The first matching case wins, so the highest band must be tested first

    Cases run top to bottom. grade(90) printed A only because >= 90 was tried before >= 75.

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:

nonexhaustive.dart: a bool switch missing false Dart · nonexhaustive.dart
// 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:

exhaustive.dart: both cases, so it compiles Dart · exhaustive.dart
// 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

  1. One question Why does a switch expression over a bool with only a true case fail to compile?

    Choose one answer.

    Show the answer: One question

    Answer: It does not match false, so it is not exhaustive

    nonexhaustive.dart failed with status 254: 'bool' is not exhaustively matched since it doesn't match 'false'.

Practise: two switch expressions

Write both functions as switch expressions, not if/else chains.

  1. String statusClass(int code) returns the five HTTP classes from RFC 9110 and throws ArgumentError for a code outside 100 to 599; match it against the boundaries 100, 199, 200, 404 and 599.
  2. 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.
  3. Run both and confirm the boundaries fall on the side you intended; an off-by-one here is the usual grading bug.
  4. Remove the last case from one function and read the error: a switch expression over int needs a _, because no finite list of int values 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.

  1. Question 1 of 5 You port Dart 2 code that has break at the end of every case. What should you do in Dart 3?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: Remove the break statements; a non-empty case already ends by itself

    A trailing break is harmless but redundant in Dart 3. Keep break only to leave a case early.

  2. Question 2 of 5 What does the pattern int n do when it matches the value 7?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: It checks the value is an int and binds n to 7 for the case body

    patterns.dart used int n to print int 7, reading the bound n in the case body.

  3. Question 3 of 5 With bands A >= 90, B >= 75, C >= 35, F below, what does grade(74) return?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: C

    grade.dart printed 74 -> C, matching the first band whose test 74 satisfied.

  4. Question 4 of 5 For which types does the compiler check a switch expression is exhaustive?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: bool, enums and sealed classes

    A closed set of values lets the compiler prove coverage; open types such as int need a wildcard.

  5. Question 5 of 5 A switch expression over a sealed shape type misses one subtype. Which fix keeps the compiler's help?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: Add a case for the missing subtype by name

    Naming cases turns a new subtype into a compile error you can find; a _ hides it. exhaustive.dart named both bool cases for the same reason.

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.

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

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

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

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

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

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

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

  6. 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) and when guards, 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, enum or 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.