skip to content

In a Dart 3 switch statement, do cases fall through, and when do you still write break or continue inside a case?

level: juniorimportance: must knowfreq 50%

answer

  1. non-empty versus empty case bodies
  2. jump to the end of the switch
  3. old Dart made break mandatory
  4. a label on the target case
  5. break leaves the switch, not the loop

basics

~20 s

Dart 3 switch statements never fall through from a non-empty case: its body runs and control leaves the switch, so no break is needed. Empty cases share the next body, and continue with a label jumps into another case.

solid answer

~40 s

In Dart 3, a non-empty `case` body completes and jumps to the end of the `switch`; `break` is optional and the `unnecessary_breaks` lint flags a trailing one. An empty case falls through, so `case 'DENIED': case 'CLOSED': closeIt();` runs `closeIt()` for both. Before Dart 3.0 a non-empty case had to end with `break`, `continue`, `return` or `throw`, or it was a compile error, so Dart has never silently fallen through like C. You still write `break` as the body of an empty case that must do nothing, and `continue someLabel;` when you deliberately want another labelled case's body to run. Inside a loop, an unlabelled `break` in a case only exits the switch.

code

dart · 20 lines
dart
enum Filing { single, joint, headOfHousehold, exempt }

double standardDeduction(Filing filing) {
  var deduction = 0.0;
  switch (filing) {
    case Filing.single:
      deduction = 14000;
    case Filing.joint:
      deduction = 28000;
    case Filing.headOfHousehold:
      deduction = 21000;
    case Filing.exempt:
      break; // an empty body that must do nothing
  }
  return deduction;
}

void main() {
  print(standardDeduction(Filing.single)); // 14000.0, no fall-through
}

go deeper

for a junior

Recall that a non-empty case runs and leaves the switch without needing break, and that empty cases share the next body.

for a middle

Explain the Dart 3.0 change: break was mandatory, never implicit fall-through, and now the jump is automatic. Show continue with a labelled case for explicit fall-through.

for a senior

Spot the migration and loop traps: redundant breaks left after an upgrade, and a break in a case that was meant to leave the surrounding loop.

for a principal

Decide team conventions: when a labelled continue is acceptable versus a shared helper, and whether to enable unnecessary_breaks once every package is on language 3.0.

## The rule in one line In a Dart 3 `switch` **statement**, a **non-empty** `case` body runs and then control jumps to the end of the switch. There is no implicit fall-through into the next case, and no `break` is needed. An **empty** case, one with no statements, falls through to the next case so that several cases can share one body. ## How it looks ```dart switch (command) { case 'OPEN': executeOpen(); // runs, then leaves the switch case 'DENIED': // empty: falls through case 'CLOSED': executeClosed(); // runs for DENIED and CLOSED default: executeUnknown(); // runs when no case matches } ``` - **`default`** or the wildcard **`_`** handles values no case matched. - A non-empty case may also end with `continue`, `throw` or `return`; `break` still works but is redundant at the end of a body. The `unnecessary_breaks` lint flags that leftover `break`. - An **empty case that should do nothing** (rather than fall through) gets `break;` as its only statement. ## What changed in Dart 3.0, and what did not | | Before Dart 3.0 | Dart 3.0 and later | |---|---|---| | Non-empty case without `break` | compile error (`switch_case_completes_normally`) | allowed; jumps to the end | | Implicit fall-through from a non-empty case | never | never | | Empty case | falls through | falls through | | What a `case` holds | a constant expression | a **pattern** (constants still work) | The important point for an interview: Dart has **never** silently fallen through from a non-empty case. Before 3.0 the language forced you to write `break` (or `continue`, `return`, `throw`) and reported an error otherwise; Dart 3.0 simply made the `break` implicit. Code ported from a C-family language that relies on fall-through does not compile in old Dart and does not fall through in new Dart. ## Deliberate fall-through: `continue` with a label When one case really should run another case's body afterwards, you say so explicitly. Put a **label** on the target case and end the source case with `continue label;`: ```dart switch (command) { case 'OPEN': executeOpen(); continue closed; // jump into the CLOSED body closed: case 'CLOSED': executeClosed(); } ``` This is rare in practice, and reviewers usually prefer a shared helper function, but it is the only way to get a non-sequential or multi-body flow in a switch statement. ## `break` inside a switch inside a loop Inside a `switch`, an unlabelled `break` ends the **switch**, not an enclosing loop. That surprises developers who expect `break` to leave the loop: 1. The `break` exits the switch. 2. The loop continues with its next iteration. 3. To leave the loop from inside a case, put a label on the loop and write `break loopLabel;`, or `return` from the function. ## Related rules you should mention but not confuse - **Cases are patterns** since 3.0, so a case can be a constant, a range with relational patterns or several alternatives joined with `||`. Pattern syntax itself is its own topic. - **Switch statements on enums, sealed types and `bool` are checked for exhaustiveness**: the analyzer reports `non_exhaustive_switch_statement` if a value could slip through with no matching case. Adding a `default` silences the check for future enum values too. - **Switch expressions** (`final label = switch (x) { ... };`) are a different construct with their own rules: no empty cases, no fall-through at all, `=>` bodies. ## What an interviewer listens for - "Non-empty cases don't fall through and don't need `break` in Dart 3." - "Empty cases share the next body." - "Old Dart required `break`; it was never a silent fall-through." - "`continue label` is the explicit fall-through; `break` in a switch only leaves the switch."

  • How do you make one Dart switch case run another case's body after its own?
    Label the target case and end the source case with `continue label;`, for example `closed:` before `case 'CLOSED':` and `continue closed;` at the end of the OPEN body. Control then jumps into the CLOSED body. It is explicit by design; most reviewers would still prefer extracting the shared work into a function.
  • Inside a for loop, what does an unlabelled break inside a Dart switch case exit?
    Only the `switch`. The loop carries on with its next iteration. To stop the loop from inside a case, label the loop (`outer: for (...)`) and write `break outer;`, or return from the enclosing function.
  • Why was old Dart code full of break statements if Dart never fell through?
    Before Dart 3.0 the language required every non-empty case to end in `break`, `continue`, `return` or `throw`; omitting it was a compile error rather than a fall-through. Dart 3.0 made the jump to the end implicit, so those `break` statements are now redundant, and the `unnecessary_breaks` lint can find them.

saying these in an interview costs you the question

  • Says Dart 3 cases fall through without break, as in C
  • Believes older Dart silently fell through when break was missing
  • Thinks an empty case is a compile error
  • Expects break inside a switch to exit the enclosing loop
  • Reaches for a fallthrough keyword that Dart does not have