Dart (3.13) Module 3 – Control flow and functions
if, else and loops: for, while and do-while
How if, else and the conditional expression pick a path, how for, for-in, while and do-while repeat work, and why each loop turn gets a fresh variable.
What you will learn
- Write if/else chains and the conditional expression to choose a path or a value
- Use for, for-in, while and do-while loops with break, continue and labels
- Explain why a closure made in a Dart for loop sees a fresh variable each turn
- Fix the error Dart throws when a list is changed while a loop reads it
Before you start
What you’ll learn · Before you start
What you will learn
- Write if/else chains and the conditional expression to choose a path or a value
- Use for, for-in, while and do-while loops with break, continue and labels
- Explain why a closure made in a Dart for loop sees a fresh variable each turn
- Fix the error Dart throws when a list is changed while a loop reads it
Before you start
On this page
A program that only runs its lines top to bottom cannot react to anything. Control flow gives it choices: if and
else pick which block runs, the conditional expression ? : picks a value, and four loops repeat work, for and
for-in for a known number of turns and while and do-while until something changes.
You will write an if/else chain and a conditional expression, loop over a list and a map, leave a nested loop with a
labelled break, see why three closures made in one loop return 0, 1 and 2 rather than 3, 3, 3, repair the error Dart
throws when a list changes mid-loop, and learn why assert guards nothing in an ordinary run.
if, else and the conditional expression
branches.dart writes the classic FizzBuzz for 1 to 15 with an if/else-if chain, then picks a greeting with a conditional expression:
import 'dart:io';
// FizzBuzz from 1 to 15 with an if/else-if chain, then a conditional expression.
void main() {
for (var n = 1; n <= 15; n++) {
String say;
if (n % 15 == 0) {
say = 'FizzBuzz';
} else if (n % 3 == 0) {
say = 'Fizz';
} else if (n % 5 == 0) {
say = 'Buzz';
} else {
say = '$n';
}
stdout.write('$say ');
}
stdout.writeln();
// An if is a statement and runs code; to choose a value, use the conditional
// expression condition ? a : b, which an assignment can hold.
const hour = 14;
final greeting = hour < 12 ? 'Good morning' : 'Good afternoon';
print(greeting);
} Output
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz Good afternoon
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run branches.dart
The first line printed 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz: each number ran exactly one branch,
the first whose test was true, so 15 printed FizzBuzz because n % 15 == 0 was tested first. Every body is wrapped in
braces, which Effective Dart’s style guide asks for on all flow-control statements; the one case it allows without them
is a short if with no else written on a single line.
An if is a statement: it runs a block, but it is not a value, so you cannot assign the result of an if to a
variable. To choose a value, Dart has the conditional expression: Dart's test ? a : b operator: an expression with a value, which evaluates a when test is true and b when it is false. Unlike an if statement, its result can be assigned or returned.
condition ? a : b, which is an expression and does have a value. Here hour < 12 ? 'Good morning' : 'Good afternoon'
gave greeting its value, and with hour set to 14 the program printed Good afternoon. The table sets the two tools
side by side:
| Tool | Shape | Is it a value? | Use it to |
|---|---|---|---|
if / else statement |
if (test) { … } else { … } |
no | run one block or another |
| conditional expression | test ? a : b |
yes | choose between two values |
A branch without braces
if (ok) first(); second(); reads as if both calls are guarded, but only first() is: second() always runs. Put
braces around every body, and the trap disappears.
Checkpoint
Check: if, else and the conditional expression
for, for-in, while and do-while
loops.dart runs all four loops, and shows how for-in reads a list and a map:
import 'dart:io';
// The four loops, and how for-in reads a List and a Map.
void main() {
// A counting for loop, with its own loop variable i.
for (var i = 1; i <= 3; i++) {
stdout.write('for $i; ');
}
stdout.writeln();
// for-in reads any Iterable, one element at a time.
final cities = ['Pune', 'Kochi', 'Agra'];
for (final city in cities) {
stdout.write('$city; ');
}
stdout.writeln();
// A Map is not an Iterable; loop over its .entries.
final prices = {'tea': 10, 'coffee': 25};
for (final entry in prices.entries) {
stdout.write('${entry.key}=${entry.value}; ');
}
stdout.writeln();
// while tests before the body, so it may run zero times.
var left = 3;
while (left > 0) {
stdout.write('while $left; ');
left--;
}
stdout.writeln();
// do-while tests after the body, so the body always runs once.
var tries = 3;
do {
stdout.write('do $tries; ');
tries++;
} while (tries < 3);
stdout.writeln();
}
// stdout.write needs dart:io. Output
for 1; for 2; for 3; Pune; Kochi; Agra; tea=10; coffee=25; while 3; while 2; while 1; do 3;
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run loops.dart
The counting for printed for 1; for 2; for 3;: it declares a loop variable, tests it before each turn, and updates
it after. for-in reads any Iterable: A sequence a Dart for-in loop reads one element at a time, such as a List or a Set; its type is Iterable. A Map is not one, so you loop over its entries, keys or values. one element at a time, so it printed the three cities
in order. A Map is not an Iterable, so you loop over its .entries, which is why prices.entries printed
tea=10; coffee=25;. The patterns module adds a shorter form, for (final MapEntry(:key, :value) in map.entries), that
names the key and value directly.
The last two loops differ only in when they test. while tests before the body, so it printed while 3; while 2; while 1; and then stopped. do-while tests after the body, so the body always runs once: with tries starting at 3,
the condition tries < 3 was already false, yet do 3; still printed. Reach for do-while when the first turn must
happen whatever the condition says, such as reading input before checking whether to read more.
Checkpoint
Check: for, for-in, while and do-while
break, continue and labels
break leaves the nearest loop and continue skips to its next turn, but a nested loop often needs to leave the
outer one. A label makes that one statement. labels.dart searches a price list for the first pair that adds up to a
budget, and a labelled break stops both loops at the first hit:
// A label lets break leave the outer loop at once, not just the inner one.
void main() {
final prices = [120, 250, 80, 400, 170];
const budget = 330;
var found = false;
search:
for (var i = 0; i < prices.length; i++) {
for (var j = i + 1; j < prices.length; j++) {
if (prices[i] + prices[j] == budget) {
print('prices[$i] + prices[$j] = ${prices[i]} + ${prices[j]} = $budget');
found = true;
break search;
}
}
}
if (!found) print('no pair adds up to $budget');
} Output
prices[1] + prices[2] = 250 + 80 = 330
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run labels.dart
The program printed prices[1] + prices[2] = 250 + 80 = 330 and stopped: without the label, break would have left
only the inner loop, and the outer loop would have gone on comparing. continue search works the same way for the
outer loop’s next turn. The label is a name written before a loop with a colon, and break or continue names it.
Dart 3.0 and continue
Since Dart 3.0 a continue must target a loop or a switch member; the old trick of using continue to jump to any
label is gone. A plain labelled break to leave an outer loop still works.
Checkpoint
Check: break, continue and labels
A fresh loop variable on every turn
A loop variable looks like one variable that changes, but Dart gives each turn of a for loop its own copy. That
matters the moment a closure captures it. capture.dart adds three closures to a list, each returning the loop variable,
then calls them:
// Each turn of a Dart for loop gets its own i, so each closure keeps a
// different one. In JavaScript, var would share one i and print [3, 3, 3].
void main() {
final callbacks = <int Function()>[];
for (var i = 0; i < 3; i++) {
callbacks.add(() => i);
}
final results = <int>[];
for (final f in callbacks) {
results.add(f());
}
print(results);
} Output
[0, 1, 2]
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run capture.dart
The program printed [0, 1, 2], not [3, 3, 3]. Each closure kept the i of its own turn, so calling them later read
three different values. Step through what each closure refers to:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- First turn. On the first turn i is 0. The closure () => i is added to the list, and it refers to the i of this turn, not to a copy of the number 0.
- Second turn. Dart gives the second turn a brand-new i, which holds 1. The first closure still refers to its own i, so nothing it sees has changed.
- Third turn. The third turn gets another fresh i, holding 2. There are now three closures, each pointing at a different i from its own turn.
- Calling the three. Calling the closures in order reads 0, 1 and 2, so results printed [0, 1, 2]. A shared i, as JavaScript's var gives, would make all three read the final value and print [3, 3, 3].
JavaScript’s var behaves the other way: it is function-scoped, so one i is shared by every turn and three closures
would all read the final value. Dart’s for gives a fresh binding each turn, which is why the list came out
[0, 1, 2]. The lesson on
closures and function types uses this to build counters that keep their own
state.
Key idea
Each turn of a Dart for loop gets its own loop variable, so a closure made in the loop captures that turn’s value and keeps it, even after the loop has moved on.
Checkpoint
Check: A fresh loop variable each turn
Changing a list while you loop over it
A for-in loop expects the list it reads to hold still. concurrent.dart removes items from a list while it loops over
it, and Dart stops the program:
// Removing from a list while a for-in reads it throws at run time.
void main() {
final nums = [1, 2, 3, 4, 5, 6];
for (final n in nums) {
if (n.isEven) nums.remove(n);
}
print(nums);
} Output (exit status 255)
Printed as an error (standard error)
Unhandled exception: Concurrent modification during iteration: Instance(length:5) of '_GrowableList'. #0 ListIterator.moveNext (dart:_internal/iterable.dart:366:7) #1 main (concurrent.dart:4:19) #2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19) #3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run concurrent.dart
The program threw Concurrent modification during iteration and ended with status 255, the status Dart uses for an
unhandled exception. The iterator noticed that the list’s length changed under it and refused to go on, rather than
skip or repeat elements silently. The fix is to decide what to remove without looping by hand. removeWhere.dart does
the same job in one call:
// removeWhere deletes matching elements in one pass, with no loop to disturb.
void main() {
final nums = [1, 2, 3, 4, 5, 6];
nums.removeWhere((n) => n.isEven);
print(nums);
} Output
[1, 3, 5]
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run removewhere.dart
It printed [1, 3, 5]. removeWhere takes a test and deletes every element that passes it, with no loop of your own to
disturb. When you do need a loop, iterate over a copy, for (final n in [...nums]), and change the original inside it.
Deleting inside a for-in
for (final n in nums) { if (n.isEven) nums.remove(n); } throws on the first removal. Use nums.removeWhere(…), or
loop over [...nums] and change nums inside.
Checkpoint
Check: Changing a list while looping
assert checks only when it is turned on
assert states something you believe is always true, so a broken belief is caught early. But it runs only when
assertions are enabled. asserts_session.py runs one file twice, first with a plain dart run and then with
--enable-asserts:
from session import files, run
# The same file run twice: plain dart run skips assert, --enable-asserts checks it.
files("asserts.dart")
run(
"""
dart run asserts.dart
!dart run --enable-asserts asserts.dart
"""
) Output
$ dart run asserts.dart fee is -10 $ dart run --enable-asserts asserts.dart Unhandled exception: 'file://~/demo/asserts.dart': Failed assertion: line 4 pos 10: 'fee >= 0': fee must not be negative #0 _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:67:4) #1 _AssertionError._throwNew (dart:core-patch/errors_patch.dart:49:5) #2 main (file://~/demo/asserts.dart:4:10) #3 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19) #4 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12) [exit status 255]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 asserts_session.py
Plain dart run asserts.dart printed fee is -10: the assert(fee >= 0, …) did nothing, even though the fee was
negative. With --enable-asserts the same file threw Failed assertion: line 4 pos 10: 'fee >= 0': fee must not be negative and ended with status 255. Flutter enables assertions in debug mode, and development-only tools usually
enable them by default; in production code they are ignored, which is why a plain dart run and a compiled executable
walked past the check.
assert is not input validation
Because a released program skips every assert, a negative fee waved through by assert(fee >= 0) reaches the rest
of the code unchecked. Validate real input with an if that throws, such as ArgumentError, and keep assert for
catching your own bugs during development.
Checkpoint
Check: When assert runs
Practise: two functions with loops only
Write two functions using loops, not collection helpers, so the mechanics stay in view.
List<String> multiplicationTable(int n, {int upTo = 10})returns the lines1 x n = nup toupTo x n, built with aforloop.int? firstRepeated(List<int> xs)returns the first value that has already been seen, ornullwhen every value is unique; use a loop and aSetyou fill as you go.- Run your file with
dart run, then trymultiplicationTable(7, upTo: 3)andfirstRepeated([4, 2, 4, 7]): you should get three lines and4. - Check the edges:
multiplicationTable(5, upTo: 0)should give an empty list, andfirstRepeated([])should givenull.
When both behave, the self-check revisits every section:
Practice that adapts to you
Practise this lesson
7 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): Which Dart loop always runs its body at least once, and when would you use it?
do-while runs its body first and tests the condition afterwards, so the body runs at least once however the condition
starts; in loops.dart the do body printed do 3; even though its tries < 3 test was already false. while and
for test before the body, so they can run zero times. Reach for do-while when the first turn must happen whatever
the test says: read one line of input and then decide whether to read more, roll a die once and then decide whether to
roll again, show a prompt and then decide whether to show it again. For anything that might not run at all, a while
or a for reads more plainly.
Two interview questions in the bank push these loop ideas into production debugging:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A cleanup job walks a list of pending tasks and removes each finished one inside a for-in loop. In tests on small lists it works; in production it sometimes throws "Concurrent modification during iteration" and leaves tasks behind. Explain why it passed tests yet fails in production, and how you would remove the finished tasks safely.
-
Mid to senior
A developer from JavaScript builds a list of button handlers in a for loop, each meant to remember its own index. Their old JavaScript printed the final count from every handler; the Dart version prints a different index from each. Explain the difference, and where capture can still bite in Dart.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does the conditional expression test ? a : b differ from if and else?
if and else run one block or another, while test ? a : b is a value you can assign or pass. Wrap every if, else and loop body in braces, so a stray statement is never left unguarded.
-
Which Dart loop always runs its body at least once?
do-while, because it tests after the body; while and for test before it and can run zero times. for-in reads any Iterable, and a map through its .entries.
-
How do you leave an outer loop from inside a nested one?
Put a label before the outer loop and write break with that label. Since Dart 3.0 a continue must target a loop or a switch member; continue to any other label is an error.
-
Three closures made in one Dart for loop return the loop variable. What do they give?
[0, 1, 2]. Each turn of a for loop gets its own loop variable, so each closure keeps that turn's value; JavaScript's shared var would give [3, 3, 3].
-
What happens when you remove items from a list inside its own for-in loop?
It throws "Concurrent modification during iteration" on the first removal. Use removeWhere, or loop over a copy such as [...nums] and change the original inside.
-
Why is assert no way to validate input?
A plain dart run and a compiled program ignore every assert, so assert(fee >= 0) let a negative fee through; only --enable-asserts made it throw. Validate real input with an if that throws, such as ArgumentError, and keep assert for your own bugs during development.
Key takeaways
ifandelserun one block or another, while the conditional expressiontest ? a : bis a value you can assign; wrap every flow-control body in braces so a stray statement is never left unguarded.forcounts,for-inreads any Iterable (a map through its.entries),whiletests before the body anddo-whiletests after it, so ado-whilebody always runs once.- A label before a loop lets
breakleave an outer loop andcontinuemove to its next turn; since Dart 3.0 acontinuemust target a loop or a switch member. - Each turn of a
forloop gets its own loop variable, so three closures made in one loop returned[0, 1, 2], where JavaScript’s sharedvarwould give[3, 3, 3]. - Changing a list inside its own
for-inthrowsConcurrent modification during iteration; useremoveWhereor loop over a copy, and keepassertfor development bugs, never for validating input a released program must reject.
Terms in this lesson
- conditional expression
- Dart's test ? a : b operator: an expression with a value, which evaluates a when test is true and b when it is false. Unlike an if statement, its result can be assigned or returned.
- Iterable
- A sequence a Dart for-in loop reads one element at a time, such as a List or a Set; its type is Iterable. A Map is not one, so you loop over its entries, keys or values.
References
- Branches (The Dart project (Google))
- Loops (The Dart project (Google))
- Effective Dart: Style (The Dart project (Google))
- Error handling (The Dart project (Google))
- var statement (MDN Web Docs (Mozilla))
Report a problem with this lesson
Kept only in this browser. Your Learn progress