Beginner

Control Flow in Dart

Learn conditionals, loops, switch statements, and pattern matching in Dart with practical Docker-ready examples

Control flow is how a program decides what to do and how many times to do it. Dart gives you a familiar C-style toolkit — if/else, for, while, switch — but layers modern features on top: arrow-style conditional expressions, collection-if and collection-for inside literals, and the powerful pattern-matching switch expressions introduced in Dart 3.

As a multi-paradigm language, Dart lets you write straightforward imperative loops and expression-oriented code that reads more like a functional language. A switch can be a statement that runs side effects, or an expression that produces a value. The same applies to conditionals: there’s the classic ternary ?: and the null-aware ?? operator for the very common “use this, or a default” case.

In this tutorial you’ll learn how to branch with if/else, choose among many cases with switch, iterate with for and while, control loops with break and continue, and use Dart’s expression-based conditionals to write concise, readable code.

Conditionals: if, else, and ternary

The if statement evaluates a boolean condition. Unlike some languages, Dart requires the condition to be an actual bool — there’s no “truthy” coercion of integers or strings.

Create a file named conditionals.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void main() {
  int temperature = 18;

  // Classic if / else if / else
  if (temperature > 30) {
    print('It is hot');
  } else if (temperature >= 15) {
    print('It is mild');
  } else {
    print('It is cold');
  }

  // Ternary conditional expression: condition ? valueIfTrue : valueIfFalse
  String label = temperature >= 15 ? 'comfortable' : 'chilly';
  print('Today is $label');

  // Null-aware ?? operator: use the right side when the left is null
  String? nickname;
  print('Hello, ${nickname ?? 'guest'}');
}

Two expression forms worth remembering:

  • Ternary ?: — picks between two values based on a condition.
  • Null-coalescing ?? — returns its left operand unless it’s null, in which case it returns the right. This pairs naturally with Dart’s sound null safety.

Switch statements and expressions

Dart’s switch comes in two flavors. As a statement it runs code for the matching case. Dart never implicitly falls through from one non-empty case to the next, but it doesn’t auto-break either: a non-empty case must explicitly end with break (or return/continue/throw) unless it’s the final clause — omitting it is a compile error. As an expression (Dart 3+) it evaluates to a value, using => for each case and a _ wildcard for the default.

Create a file named switch_demo.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void main() {
  // Switch statement
  String day = 'SAT';
  switch (day) {
    case 'SAT':
    case 'SUN':
      print('Weekend');
      break;
    default:
      print('Weekday');
  }

  // Switch expression (Dart 3) - returns a value
  int score = 85;
  String grade = switch (score ~/ 10) {
    10 || 9 => 'A',
    8 => 'B',
    7 => 'C',
    _ => 'F',
  };
  print('Grade: $grade');
}

Notice the || logical-or pattern (10 || 9) that matches multiple values in a single case, and the _ wildcard that handles everything else. The ~/ operator is integer division, so 85 ~/ 10 is 8.

For loops and while loops

Dart supports the standard counting for loop, the for-in loop for iterating over collections, and while / do-while loops.

Create a file named loops.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void main() {
  // Counting for loop
  for (int i = 1; i <= 3; i++) {
    print('Count: $i');
  }

  // for-in loop over a list
  var fruits = ['apple', 'banana', 'cherry'];
  for (var fruit in fruits) {
    print('Fruit: $fruit');
  }

  // while loop
  int countdown = 3;
  while (countdown > 0) {
    print('T-minus $countdown');
    countdown--;
  }

  // do-while always runs the body at least once
  int n = 0;
  do {
    print('Runs once even though n is $n');
  } while (n > 0);
}

The for-in loop is the idiomatic way to walk a collection. Lists also offer a functional forEach method, but for-in integrates cleanly with break and continue.

Loop control: break and continue

break exits the nearest enclosing loop immediately; continue skips to the next iteration. Labels let you target an outer loop from within a nested one.

Create a file named loop_control.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void main() {
  // continue: skip even numbers
  for (int i = 1; i <= 6; i++) {
    if (i % 2 == 0) continue;
    print('Odd: $i');
  }

  // break: stop at the first match
  var names = ['Ada', 'Linus', 'Grace'];
  for (var name in names) {
    if (name == 'Linus') {
      print('Found Linus, stopping');
      break;
    }
    print('Checked $name');
  }

  // Labeled break to escape a nested loop
  outer:
  for (int row = 0; row < 3; row++) {
    for (int col = 0; col < 3; col++) {
      if (row + col == 3) {
        print('Breaking outer at row=$row col=$col');
        break outer;
      }
    }
  }
}

Control flow inside collections

A feature unique to Dart’s expressiveness: you can use if and for inside list, set, and map literals. This is called collection-if and collection-for, and it lets you build collections declaratively without a separate loop.

Create a file named collection_flow.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void main() {
  bool includeBonus = true;

  // collection-if and collection-for inside a list literal
  var items = [
    'base',
    if (includeBonus) 'bonus',
    for (int i = 1; i <= 3; i++) 'item$i',
  ];

  print(items);
  print('Total items: ${items.length}');
}

The resulting list is built in one expression: the if conditionally adds 'bonus', and the for expands into three generated entries.

Running with Docker

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull dart:stable

# Run each example
docker run --rm -v $(pwd):/app -w /app dart:stable dart run conditionals.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run switch_demo.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run loops.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run loop_control.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run collection_flow.dart

Expected Output

Running conditionals.dart:

It is mild
Today is comfortable
Hello, guest

Running switch_demo.dart:

Weekend
Grade: B

Running loops.dart:

Count: 1
Count: 2
Count: 3
Fruit: apple
Fruit: banana
Fruit: cherry
T-minus 3
T-minus 2
T-minus 1
Runs once even though n is 0

Running loop_control.dart:

Odd: 1
Odd: 3
Odd: 5
Checked Ada
Found Linus, stopping
Breaking outer at row=1 col=2

Running collection_flow.dart:

[base, bonus, item1, item2, item3]
Total items: 5

Key Concepts

  • Conditions must be bool — Dart has no truthy/falsy coercion, so if (0) is a compile error; you must write an explicit comparison.
  • Switch doesn’t fall through — non-empty cases must end with an explicit break (or return/continue/throw) unless they’re the last clause, while empty stacked cases (like case 'SAT': case 'SUN':) share the next case’s body.
  • Switch expressions return values — use switch (x) { pattern => value, _ => default } with => and a _ wildcard for concise, value-producing branching (Dart 3+).
  • Multiple loop styles — counting for, for-in for collections, while, and do-while (which always runs once) cover every iteration need.
  • break and continue — exit or skip iterations; labeled breaks let you escape an outer loop from inside nested loops.
  • Collection-if and collection-for — embed control flow directly inside list, set, and map literals to build collections declaratively.
  • Null-aware ?? — a clean way to express “use this value, or fall back to a default” that complements if-based null checks.

Running Today

All examples can be run using Docker:

docker pull dart:stable
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining