Skip to main content

Control Flow

Dart gives you the full toolkit: classic conditionals and loops, modern pattern-based switching, async iteration, and labelled jumps. This chapter covers all of them with real-world context.


if / else

int score = 85;

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

// Always use braces — even for single statements
// ✅ Clear
if (score > 50) {
print('Pass');
}
// ⚠️ Error-prone (next dev might add a line and break the logic)
if (score > 50) print('Pass');

Ternary Operator

Use for simple two-branch assignments. Avoid nesting more than once.

var label = score >= 50 ? 'Pass' : 'Fail';

// One level of nesting is OK
var grade = score >= 90 ? 'A' : (score >= 80 ? 'B' : 'C');

// Too deep → use a switch expression or helper function instead
String classify(int s) => switch (s) {
>= 90 => 'A',
>= 80 => 'B',
>= 70 => 'C',
>= 60 => 'D',
_ => 'F',
};

Null-Short-Circuit Patterns

// Guard clause — return early rather than nesting
String processUser(User? user) {
if (user == null) return 'No user';
if (!user.isActive) return 'User inactive';
if (user.name.isEmpty) return 'Name missing';

// Happy path is now un-nested
return user.name.toUpperCase();
}

switch Statement

Classic switch (pre-Dart 3)

var day = 'Monday';

switch (day) {
case 'Monday':
case 'Tuesday':
case 'Wednesday':
case 'Thursday':
case 'Friday':
print('Weekday');
break; // ← required in classic style to prevent fall-through
case 'Saturday':
case 'Sunday':
print('Weekend');
break;
default:
print('Unknown');
}

Modern switch Statement (Dart 3 — no break needed)

// Dart 3 switch statement — each case has its own implicit break
switch (day) {
case 'Saturday' || 'Sunday':
print('Weekend!');
case 'Monday' || 'Tuesday' || 'Wednesday' || 'Thursday' || 'Friday':
print('Weekday');
case _: // wildcard default
print('Unknown');
}

switch Expression (Dart 3 — returns a value)

// Assign the result of a switch directly
var type = switch (day) {
'Saturday' || 'Sunday' => 'Weekend',
'Monday' || 'Tuesday' || 'Wednesday' || 'Thursday' || 'Friday' => 'Weekday',
_ => 'Unknown',
};

// Great for mapping enums to values
enum Direction { north, south, east, west }

String arrow(Direction d) => switch (d) {
Direction.north => '↑',
Direction.south => '↓',
Direction.east => '→',
Direction.west => '←',
// No default needed — enum is exhaustive!
};

// Compute values inline
double taxRate(String country) => switch (country) {
'US' => 0.08,
'UK' => 0.20,
'DE' => 0.19,
'AU' => 0.10,
_ => 0.0,
};

switch with Guards (when)

int n = 42;

switch (n) {
case int x when x < 0:
print('Negative: $x');
case 0:
print('Zero');
case int x when x.isEven:
print('Positive even: $x'); // 42 matches here
case int x:
print('Positive odd: $x');
}

// Guards in switch expressions
String category(int n) => switch (n) {
< 0 => 'negative',
0 => 'zero',
int x when x.isEven && x < 100 => 'small even',
int x when x.isOdd && x < 100 => 'small odd',
_ => 'large',
};

switch with Type Patterns

Object value = 3.14;

switch (value) {
case int n: print('Integer: $n');
case double d: print('Double: $d'); // matches here
case String s: print('String: $s');
case bool b: print('Bool: $b');
case null: print('Null');
case _: print('Other: $value');
}

// Type pattern + destructure (very powerful)
void describe(Object shape) {
switch (shape) {
case Circle(radius: var r) when r > 10:
print('Big circle, r=$r');
case Circle(radius: var r):
print('Small circle, r=$r');
case Rectangle(width: var w, height: var h):
print('Rect ${w}x${h}');
case _:
print('Unknown shape');
}
}

Exhaustive switch on sealed classes

sealed class PaymentResult {}
class PaymentSuccess extends PaymentResult { final String txId; PaymentSuccess(this.txId); }
class PaymentFailed extends PaymentResult { final String reason; PaymentFailed(this.reason); }
class PaymentPending extends PaymentResult { final DateTime eta; PaymentPending(this.eta); }

// Dart verifies ALL subclasses are handled — no default needed
String message(PaymentResult r) => switch (r) {
PaymentSuccess(txId: var id) => 'Paid! Transaction: $id',
PaymentFailed(reason: var why) => 'Failed: $why',
PaymentPending(eta: var when) => 'Pending until $when',
};

// Add a new subclass → compiler error here → you can't miss it ✅

for Loops

// Classic C-style for
for (int i = 0; i < 5; i++) {
print(i); // 0 1 2 3 4
}

// Multiple variables
for (int i = 0, j = 10; i < j; i++, j--) {
print('i=$i j=$j');
}

// for-in — iterate any Iterable
var fruits = ['apple', 'banana', 'cherry'];
for (var fruit in fruits) {
print(fruit);
}

// for-in with index (Dart 3 — .indexed)
for (var (i, fruit) in fruits.indexed) {
print('$i: $fruit');
}
// 0: apple 1: banana 2: cherry

// forEach — concise, but no break/continue
fruits.forEach(print); // just print
fruits.forEach((f) => print(f.toUpperCase()));

// When you need break/continue → use for-in, not forEach
for (var fruit in fruits) {
if (fruit == 'banana') continue; // skip banana
print(fruit);
}

Iterating Maps

var scores = {'Alice': 95, 'Bob': 82, 'Carol': 78};

// Entries
for (var entry in scores.entries) {
print('${entry.key}: ${entry.value}');
}

// Destructured (Dart 3)
for (var MapEntry(key: name, value: score) in scores.entries) {
print('$name scored $score');
}

// Keys only / values only
for (var name in scores.keys) { print(name); }
for (var score in scores.values){ print(score); }

while & do-while

// while — condition BEFORE body (may never run)
int i = 0;
while (i < 5) {
print(i++);
}

// do-while — condition AFTER body (runs at least once)
int attempts = 0;
bool connected = false;
do {
connected = tryConnect();
attempts++;
} while (!connected && attempts < 3);
print(connected ? 'Connected!' : 'Gave up after $attempts attempts');

// Classic event loop pattern
while (true) {
var command = readCommand();
if (command == 'exit') break;
execute(command);
}

break & continue

// break — exit the nearest enclosing loop or switch
for (var i = 0; i < 100; i++) {
if (i * i > 50) {
print('First i where i² > 50 is $i');
break;
}
}

// continue — skip rest of body, go to next iteration
var sum = 0;
for (var i = 1; i <= 10; i++) {
if (i % 3 == 0) continue; // skip multiples of 3
sum += i;
}
print(sum); // 1+2+4+5+7+8+10 = 37

Labelled break / continue

Used to jump out of nested loops — use sparingly; extracting to a function is usually cleaner.

// continue outer — skip to next i without completing inner loop
outerLoop:
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (j == 2) continue outerLoop;
print('($i, $j)');
}
}
// (0,0) (0,1) (1,0) (1,1) (2,0) (2,1) (3,0) (3,1)

// break outer — exit both loops entirely
search:
for (var row = 0; row < matrix.length; row++) {
for (var col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] == target) {
print('Found at ($row, $col)');
break search;
}
}
}

// ✅ Cleaner alternative — extract to a function and use return
(int, int)? findInMatrix(List<List<int>> m, int target) {
for (var r = 0; r < m.length; r++) {
for (var c = 0; c < m[r].length; c++) {
if (m[r][c] == target) return (r, c);
}
}
return null;
}

assert

Asserts enforce developer invariants in debug mode. They compile out completely in production (AOT/release builds).

int divide(int a, int b) {
assert(b != 0, 'Divisor b must not be zero');
return a ~/ b;
}

// Assert expressions can be complex
void setAge(int age) {
assert(age >= 0 && age <= 150, 'Age $age is not realistic');
_age = age;
}

// Assert in constructors (via initializer list)
class PositiveValue {
final int value;
PositiveValue(this.value) : assert(value > 0, 'Must be positive, got $value');
}

// Enable in debug runs:
// dart run --enable-asserts app.dart
// Flutter debug mode always has assertions enabled.
// Never rely on asserts for production validation — use ArgumentError instead.

if-case (Dart 3)

Combines a type/structure check with binding — cleaner than is + cast:

Object response = {'status': 200, 'body': 'Hello'};

// Destructure and bind in one step
if (response case {'status': 200, 'body': String body}) {
print('Success: $body');
} else if (response case {'status': int code, 'error': String msg}) {
print('HTTP $code: $msg');
} else {
print('Unexpected response format');
}

// With type patterns
dynamic json = fetchSomething();
if (json case List<dynamic> items when items.isNotEmpty) {
print('Got ${items.length} items, first: ${items.first}');
}

// Very clean for unwrapping sealed classes
if (result case Ok(value: var data)) {
renderData(data);
}

Loops with Async

Sequential async iteration

// await for — iterate a Stream one event at a time
Stream<String> readLines(String path) async* {
final file = File(path);
await for (var line in file.openRead().transform(utf8.decoder).transform(LineSplitter())) {
yield line;
}
}

Future<void> main() async {
await for (var line in readLines('data.txt')) {
print(line);
}
print('Done reading');
}

Sequential vs parallel async loops

// Sequential — waits for each before starting next (safe, slow)
Future<void> sequential(List<String> urls) async {
for (var url in urls) {
var data = await fetch(url); // each awaited in turn
process(data);
}
}

// Parallel — all requests fire at once (fast, but watch concurrency limits)
Future<void> parallel(List<String> urls) async {
var results = await Future.wait(urls.map(fetch));
results.forEach(process);
}

// Controlled concurrency — N at a time
Future<void> batched(List<String> urls, {int batchSize = 5}) async {
for (var i = 0; i < urls.length; i += batchSize) {
var batch = urls.sublist(i, (i + batchSize).clamp(0, urls.length));
var results = await Future.wait(batch.map(fetch));
results.forEach(process);
}
}

Summary

ConstructSyntaxNotes
Conditionalif (cond) { } else { }Always use braces
Ternarycond ? a : bOne level deep only
Classic switchswitch (x) { case v: break; }Needs break
Dart 3 switch stmtswitch (x) { case v: }No break needed
Switch expressionswitch (x) { v => expr, }Returns a value
Guard clausecase T x when x > 0:Filter within a case
Classic forfor (int i = 0; i < n; i++)Full control
for-infor (var x in iterable)Clean iteration
Indexed for-infor (var (i, x) in list.indexed)Dart 3
whilewhile (cond) { }Check-before
do-whiledo { } while (cond)Check-after, runs once
breakbreak; / break label;Exit loop/switch
continuecontinue; / continue label;Next iteration
assertassert(cond, 'msg')Debug-only invariant
if-caseif (x case Pattern p)Match + bind, Dart 3
await forawait for (var x in stream)Async stream loop