Skip to content

03 · Control Flow

if / else if / else

void main() {
  int score = 82;

  if (score >= 90) {
    print('A');
  } else if (score >= 80) {
    print('B');
  } else if (score >= 70) {
    print('C');
  } else {
    print('F');
  }
  // B
}

Dart requires the condition to be a bool — there's no implicit truthiness for numbers or strings, unlike JavaScript or Python.

// if (0) { ... }        // compile error: int is not bool
// if ('') { ... }        // compile error: String is not bool

switch statements and expressions

Dart's switch supports classic case statements with fallthrough control, plus (since Dart 3) a more concise switch expression form.

void main() {
  String day = 'TUE';

  // Classic switch statement
  switch (day) {
    case 'MON':
    case 'TUE':
    case 'WED':
    case 'THU':
    case 'FRI':
      print('Weekday');
      break;
    case 'SAT':
    case 'SUN':
      print('Weekend');
      break;
    default:
      print('Unknown');
  }
  // Weekday
}
void main() {
  int month = 4;

  // Switch expression -- evaluates to a value directly, no break needed
  String season = switch (month) {
    12 || 1 || 2 => 'Winter',
    3 || 4 || 5 => 'Spring',
    6 || 7 || 8 => 'Summer',
    9 || 10 || 11 => 'Fall',
    _ => 'Invalid month',
  };

  print(season);   // Spring
}

The switch expression form is exhaustiveness-checked by the compiler and is generally preferred in modern Dart when you're producing a value.

for loops

void main() {
  // Classic C-style for loop
  for (int i = 0; i < 3; i++) {
    print('i = $i');
  }
  // i = 0
  // i = 1
  // i = 2

  // for-in loop over an iterable
  var fruits = ['apple', 'banana', 'cherry'];
  for (var fruit in fruits) {
    print(fruit);
  }
  // apple
  // banana
  // cherry
}

while and do-while

void main() {
  int countdown = 3;
  while (countdown > 0) {
    print(countdown);
    countdown--;
  }
  print('Liftoff!');
  // 3
  // 2
  // 1
  // Liftoff!

  int n = 0;
  do {
    print('runs at least once, n=$n');
    n++;
  } while (n < 0);
  // runs at least once, n=0
}

break and continue

void main() {
  for (int i = 0; i < 10; i++) {
    if (i == 5) break;          // exit the loop entirely
    if (i.isOdd) continue;      // skip the rest of this iteration
    print(i);
  }
  // 0
  // 2
  // 4
}

Labeled loops

Dart supports labels so break/continue can target an outer loop from inside a nested one:

void main() {
  outer:
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      if (j == 1) continue outer;
      print('i=$i j=$j');
    }
  }
  // i=0 j=0
  // i=1 j=0
  // i=2 j=0
}

Ternary and null-aware operators

void main() {
  int age = 20;
  String category = age >= 18 ? 'adult' : 'minor';
  print(category);   // adult

  String? nickname;
  String displayName = nickname ?? 'Anonymous';   // ?? -- use right side if left is null
  print(displayName);   // Anonymous

  nickname ??= 'Ada';   // assign only if currently null
  print(nickname);      // Ada
}

Cheat sheet

Construct Use when
if/else if/else Branching on arbitrary boolean conditions
switch statement Branching on discrete values with side effects per case
switch expression Producing a value from discrete cases, exhaustively checked
for Known number of iterations, or iterating a collection
while Unknown number of iterations, condition checked before body
do-while Same, but body always runs at least once
break / continue Exit a loop early / skip to the next iteration
?: Inline conditional expression
?? / ??= Fallback for null, or assign-if-null

How It Actually Works

Dart's newer switch expressions (as opposed to switch statements) are compiled using exhaustiveness analysis against the matched type: if you switch over an enum or a sealed class hierarchy, the compiler proves at compile time whether every case is covered, and refuses to compile if it isn't (unless you add a default/wildcard _). This isn't just a lint — it's the analyzer walking the declared subtype set of the matched value's static type, which is why exhaustiveness checking only works reliably on closed type hierarchies (enums, sealed classes), not on arbitrary open classes.

break, continue, and labeled loops compile to jump instructions in the underlying IR, but the "label" itself is purely a compile-time construct — by the time code reaches the VM's intermediate representation or the AOT backend, labels have already been resolved into direct control-flow edges between basic blocks. There's no runtime cost to a label versus an unlabeled loop; the label only exists to disambiguate which enclosing loop a break/continue targets during compilation.

The null-aware operators (??, ?.) are not simply syntactic sugar evaluated left-to-right at runtime with a null check — the compiler's type-flow analysis uses them as promotion evidence. After if (x != null) or x?.something, Dart's flow analysis can promote x's static type from T? to T for the rest of that scope, meaning subsequent member accesses on x skip the null check entirely and compile to a direct (non-nullable) call — a real compile-time optimization, not just a readability nicety.

🔀 See this in another language

Exercise

Write a program that loops from 1 to 30 and, for each number, prints "Fizz" if divisible by 3, "Buzz" if divisible by 5, "FizzBuzz" if divisible by both, and the number itself otherwise. Then write a switch expression that maps an int 1-7 to its weekday name, and print the result for a couple of test values including an out-of-range one (using _ as the default case).