Contents

Branches

This page shows how you can control the flow of your Dart code using branches:

  • if statements and elements
  • if-case statements and elements
  • switch statements and expressions

You can also manipulate control flow in Dart using:

If

Dart supports if statements with optional else clauses. The condition in parentheses after if must be an expression that evaluates to a boolean:

if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}

To learn how to use if in an expression context, see Conditional expressions

If-case

Dart if statements support case clauses followed by a pattern:

if (pair case [int x, int y]) return Point(x, y);

If the pattern matches the value, then the branch executes with any variables the pattern defines in scope.

In the previous example, the list pattern [int x, int y] matches the value pair, so the branch return Point(x, y) executes with the variables that the pattern defined, x and y.

Otherwise, control flow progresses to the else branch to execute, if there is one:

if (pair case [int x, int y]) {
  print('Was coordinate array $x,$y');
} else {
  throw FormatException('Invalid coordinates.');
}

The if-case statement provides a way to match and destructure against a single pattern. To test a value against multiple patterns, use switch.

Switch statements

A switch statement evaluates a value expression against a series of cases. Each case clause is a pattern for the value to match against. You can use any kind of pattern for a case.

When the value matches a case’s pattern, the case body executes. Non-empty case clauses jump to the end of the switch after completion. They do not require a break statement. Other valid ways to end a non-empty case clause are a continue, throw, or return statement.

Use a default or wildcard _ clause to execute code when no case clause matches:

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
  case 'PENDING':
    executePending();
  case 'APPROVED':
    executeApproved();
  case 'DENIED':
    executeDenied();
  case 'OPEN':
    executeOpen();
  default:
    executeUnknown();
}

Empty cases fall through to the next case. For an empty case that does not fall through, use break for its body. For non-sequential fall-through, you can use a continue statement and a label:

switch (command) {
  case 'OPEN':
    executeOpen();
    continue newCase;     // Continues executing at the newCase label.
  
  case 'DENIED':          // Empty case falls through.
  case 'CLOSED':
    executeClosed();      // Runs for both DENIED and CLOSED,
  
  newCase:
  case 'PENDING':
    executeNowClosed();   // Runs for both OPEN and PENDING.
}

You can use logical-or patterns to allow cases to share a body or a guard. To learn more about patterns and case clauses, check out the patterns documentation on Switch statements and expressions.

Switch expressions

A switch expression produces a value based on the expression body of whichever case matches. You can use a switch expression wherever Dart allows expressions, except at the start of an expression statement. For example:

var x = switch (y) { ... };

print(switch (x) { ... });

return switch (x) { ... };

If you want to use a switch at the start of an expression statement, use a switch statement.

Switch expressions allow you to rewrite a switch statement like this:

// Where slash, star, comma, semicolon, etc., are constant variables...

switch (charCode) {
  case slash || star || plus || minus:    // Logical-or pattern
    token = operator(charCode);
  case comma || semicolon:                // Logical-or pattern
    token = punctuation(charCode);
  case >= digit0 && <= digit9:            // Relational and logical-and patterns
    token = number();
  default:
    throw invalid();
}

Into an expression, like this:

token = switch (charCode) {
  slash || star || plus || minus => operator(charCode),
  comma || semicolon => punctuation(charCode),
  >= digit0 && <= digit9 => number(),
  _ => throw invalid()
};

The syntax of a switch expression differs from switch statement syntax:

  • Cases do not start with the case keyword.
  • A case body is a single expression instead of a series of statements.
  • Each case must have a body; there is no implicit fallthrough for empty cases.
  • Case patterns are separated from their bodies using => instead of :.
  • Cases are separated by , (and an optional trailing , is allowed).
  • Default cases can only use _, instead of allowing both default and _.

Exhaustiveness checking

Exhaustiveness checking is a feature that reports a compile-time error if it’s possible for a value to enter a switch but not match any of the cases.

bool? b = false;
// Non-exhaustive switch on bool?, missing case to match null possiblity:
switch (b) {
  case true: print('yes');
  case false: print('no');
}

A default case (default or _) covers all possible values that can flow through a switch. This makes a switch on any type exhaustive.

Enums and sealed types are particularly useful for switches because, even without a default case, their possible values are known and fully enumerable. Use the sealed modifier on a class to enable exhaustiveness checking when switching over subtypes of that class:

sealed class Shape {
  double calculateArea();
}

class Square extends Shape { ... }
class Circle extends Shape { ... }

// ...

double calculateArea(Shape shape) =>
  switch (shape) {
    Square(length: var l) => l * l,
    Circle(radius: var r) => math.pi * r * r
  };

If anyone were to add a new subclass of Shape, this switch expression would be incomplete. Exhaustiveness checking would inform you of the missing subtype. This allows you to use Dart in a somewhat functional algebraic datatype style.

Guard clause

To set an optional guard clause after a case clause, use the keyword when. A guard clause can follow if case, and both switch statements and expressions.

switch (pair) {
  case (int a, int b) when a > b:
    print('First element greater');
  case (int a, int b):
    print('First element not greater');
}

Guards evaluate an arbitrary boolean expression after matching. This allows you to add further constraints on whether a case body should execute. When the guard clause evaluates to false, execution proceeds to the next case rather than exiting the entire switch.