skip to content

In Dart, what is the difference between refutable and irrefutable pattern contexts, and why does a bare `case limit:` not bind a new variable?

level: middleimportance: should knowfreq 36%

answer

  1. declaration and assignment: must match
  2. if-case and switch: may fail
  3. a bare identifier in a case is a constant
  4. use var or a type to bind
  5. missing map key throws in a declaration

basics

~20 s

Declarations and assignments are irrefutable: their patterns must match or throw. if-case and switch cases are refutable: a mismatch just fails. In a case, a bare identifier is a constant pattern, so case limit: compares instead of binding.

solid answer

~50 s

Dart sorts pattern positions into two contexts. **Irrefutable** contexts are pattern variable declarations and pattern assignments: the pattern must match, so the analyzer rejects patterns that can merely fail, and a runtime mismatch it cannot rule out statically throws, such as a missing map key raising a `StateError`. **Refutable**, or matching, contexts are `if-case` and switch cases: any pattern kind is allowed, and a mismatch simply moves on to the else branch or the next case. The same identifier means different things in each: in a declaration, `var (a, b) = pair;` declares `a` and `b`; in a matching context, a bare identifier is a **constant pattern**, so `case limit:` tests `value == limit` against an existing constant. To bind in a case you write `var x`, `final x` or a typed `double x`; `_` is the one identifier that always acts as a wildcard.

code

dart · 24 lines
dart
const freezing = 0;

String describe(Object? reading) {
  if (reading case freezing) return 'exactly freezing'; // constant pattern
  if (reading case int t when t < freezing) return '$t below zero';
  if (reading case (double t, 'C')) return '$t degrees Celsius';
  return 'unknown';
}

void main() {
  print(describe(0)); // exactly freezing
  print(describe(-4)); // -4 below zero
  print(describe((3.5, 'C'))); // 3.5 degrees Celsius

  var (city, temp) = ('Oslo', 3.5); // irrefutable: declares city and temp
  print('$city $temp'); // Oslo 3.5

  try {
    final {'temp': int? t} = {}; // missing key
    print(t);
  } on StateError {
    print('declaration threw StateError'); // this line runs
  }
}

go deeper

for a junior

Remember that declarations with var or final must match, while if-case and switch cases may fail and move on.

for a middle

Explain the identifier rule: a bare name declares in a declaration, assigns in an assignment and is a constant in a case, so binding in a case needs var, final or a type.

for a senior

Choose contexts deliberately: throwing declarations for guaranteed shapes, refutable matching for untrusted data, and guards instead of relational patterns for runtime thresholds.

for a principal

Set a codebase rule for where declarations may destructure external data, so shape errors surface as handled mismatches rather than StateError or TypeError crashes.

## Two kinds of context A **refutable pattern** is one that can be tested against a value and may fail — the value "refutes" it. An **irrefutable pattern** always matches. Dart assigns every place a pattern can appear to one of two contexts: | Context | Where | What a mismatch does | |---|---|---| | **Irrefutable** | pattern variable declarations (`var`/`final`), pattern assignments, `for` / `for-in` loop variables | caught at compile time where possible; otherwise throws | | **Refutable** (matching) | `if-case`, switch statement cases, switch expression cases, collection `if-case` elements | the match fails and control moves on | Only **irrefutable** patterns may appear in an irrefutable context. Patterns whose whole job is to test — a **constant** such as `'C'`, a **relational** check such as `> 0`, a **null-check** `x?` that fails on null — have nothing to fall back to in a declaration, so they are not allowed there and belong in matching contexts. Declarations use the throwing counterparts instead: the **null-assert** pattern `x!` and the **cast** pattern `x as T`. ## What still throws in a declaration Some checks cannot be decided from static types, so an irrefutable context turns them into runtime errors. The documented example is a map pattern with a missing key: ```dart final {'temp': int? t} = {}; // throws StateError: no 'temp' key ``` Inside an if-case, the same missing key would simply make the match fail. This is the practical rule: **use declarations for shapes you already know are right, matching contexts for shapes you are checking.** ## The identifier trap An **identifier pattern** changes meaning with the context: - **Declaration context** — declares a new variable: `var (a, b) = (1, 2);` - **Assignment context** — assigns to an existing variable: `(a, b) = (3, 4);` - **Matching context** — is a **named constant pattern**: the case matches when the value equals that constant. - **`_`** — a wildcard everywhere: matches anything and binds nothing. So this does not do what a newcomer expects: ```dart const limit = 30; switch (temp) { case limit: // constant pattern: matches only when temp == 30 print('exactly the limit'); case var t when t > limit: // variable pattern plus guard print('$t is above the limit'); } ``` `case limit:` never binds a new `limit`; it compares with the constant. To bind, write `var t`, `final t` or a typed variable pattern such as `int t`. If `limit` were not a constant at all — say, a local `final` computed at run time — the case would not compile, because constant patterns need compile-time constants; the fix is a guard: `case var t when t > limit`. ## Constant patterns in more detail - Literals (`30`, `'C'`, `true`, `null`), named constants (`limit`, `double.infinity`) and `const` constructor calls are allowed directly. - Other constant expressions must be written as `const (...)`: `case const (20 + 10):`. - A list or map in a case is a **pattern**, not a literal: with constants `a` and `b`, `case [a, b]:` is a list pattern whose two elements are constant patterns, while `case const [a, b]:` compares with one constant list. To bind the elements instead, write `case [var a, var b]:`. ## Quick reference: what goes where - **Declarations and assignments** accept variables, `_`, typed variables whose type the value already has statically, record, list, map and object patterns that fit the static type, and the throwing forms `x!` and `x as T`. - **Matching contexts** accept all of those plus the test-only kinds: constant, relational, logical-or and null-check patterns, and `when` guards after a case. - **A runtime shape mismatch** in a declaration throws — for example a missing map key raises a `StateError` — while the same mismatch in an if-case or switch case just fails. - **Choosing**: data you produced yourself can be destructured in a declaration; data from outside the process is matched first. ## Why the design is this way - Declarations are about **binding**, so a bare name binds. - Cases are about **testing**, and testing against named constants — enum-like values, thresholds, sentinel strings — is the most common thing a case does, so a bare name tests. - Making the intent explicit with `var`/`final`/a type keeps a case readable: every binding is visibly a binding.

  • Why is `final (x?, y?) = position;` not the way to drop nulls from a `(int?, int?)` record?
    A null-check pattern exists to fail on null, and a declaration has nowhere to go on failure, so it is a refutable pattern in an irrefutable context. Use the null-assert form `final (x!, y!) = position;` if null is a bug and throwing is right, or an if-case with `(var x?, var y?)` if null is an expected case.
  • How do you compare a switch value with a runtime threshold such as a user setting?
    Not with a constant or relational pattern, since both need compile-time constants. Bind the value and use a guard: `case var t when t > settings.limit:`. The guard runs after the pattern matches and can use any expression.

saying these in an interview costs you the question

  • Thinks case limit: binds the switched value to a new variable
  • Believes a failed if-case match throws like a failed declaration
  • Uses a runtime variable inside a relational pattern such as > threshold
  • Assumes a declaration pattern with a missing map key yields null
  • Treats case [a, b]: as comparing against a list literal