Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 10 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

branches.dart: an if/else chain, then a conditional expression Dart · branches.dart
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

  1. Question 1 of 2 What does if (ok) first(); second(); do when ok is false?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It skips first() but still runs second()

    An if without braces guards only the one statement after it. Effective Dart asks for braces on every flow-control body so a second statement is never left unguarded.

  2. Question 2 of 2 Which line gives label the text based on a test?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: final label = big ? 'L' : 'S';

    An if runs a block; the conditional expression test ? a : b has a value. branches.dart used it to set greeting to 'Good afternoon'.

for, for-in, while and do-while

loops.dart runs all four loops, and shows how for-in reads a list and a map:

loops.dart: the four loops, over a list and a map Dart · loops.dart
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

  1. Question 1 of 2 How many times does the body of do { … } while (false); run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Once

    do-while checks its condition after the body. In loops.dart the do body printed do 3; even though tries < 3 was already false.

  2. Question 2 of 2 How do you loop over the keys and values of a Map with for-in?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Loop over map.entries and read entry.key and entry.value

    loops.dart looped over prices.entries and printed tea=10; coffee=25;. The patterns module adds a form that names key and value directly.

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:

labels.dart: a labelled break leaves both loops Dart · labels.dart
// 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

  1. One question In a nested loop, what does a labelled break such as break search; do?

    Choose one answer.

    Show the answer: One question

    Answer: It leaves the outer loop named search, not just the inner loop

    labels.dart used break search; to stop both loops at the first matching pair, printing one line and ending.

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:

capture.dart: three closures that return 0, 1 and 2 Dart · capture.dart
// 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:

Interactive · step by step Each closure keeps the i from its own turn Step through capture.dart. Each closure stores one turn's i, not the loop's final value.
Each closure keeps the i from its own turn, step 4 of 4. Showing closure 0: () => i, closure 1: () => i, closure 2: () => i, i = 0, i = 1, i = 2. In focus: closure 0: () => i, closure 1: () => i, closure 2: () => i.closure 0: () => iclosure 1: () => iclosure 2: () => ii = 0i = 1i = 2

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. One question Three closures () => i are added in for (var i = 0; i < 3; i++). What do they return when called?

    Choose one answer.

    Show the answer: One question

    Answer: 0, 1 and 2

    capture.dart printed [0, 1, 2]. Dart binds a fresh loop variable each turn, so each closure returns its own turn's value.

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:

concurrent.dart: removing from a list mid-loop Dart · concurrent.dart
// 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.dart: removeWhere deletes in one pass Dart · removewhere.dart
// 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

  1. One question What happens when you call nums.remove(n) inside for (final n in nums)?

    Choose one answer.

    Show the answer: One question

    Answer: It throws Concurrent modification during iteration

    concurrent.dart ended with status 255 and that message. removeWhere, or looping over a copy, avoids it.

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:

assert skipped by dart run, then checked with --enable-asserts Python · asserts_session.py
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

  1. One question What does assert(fee >= 0) do under a plain dart run when fee is -10?

    Choose one answer.

    Show the answer: One question

    Answer: Nothing; the assertion is skipped

    asserts_session.py printed fee is -10 under dart run, and only --enable-asserts threw the failed assertion.

Practise: two functions with loops only

Write two functions using loops, not collection helpers, so the mechanics stay in view.

  1. List<String> multiplicationTable(int n, {int upTo = 10}) returns the lines 1 x n = n up to upTo x n, built with a for loop.
  2. int? firstRepeated(List<int> xs) returns the first value that has already been seen, or null when every value is unique; use a loop and a Set you fill as you go.
  3. Run your file with dart run, then try multiplicationTable(7, upTo: 3) and firstRepeated([4, 2, 4, 7]): you should get three lines and 4.
  4. Check the edges: multiplicationTable(5, upTo: 0) should give an empty list, and firstRepeated([]) should give null.

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.

  1. Question 1 of 7 In an if/else-if chain, which branch runs?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: The first branch whose test is true, and no other

    branches.dart tested n % 15 == 0 first, so 15 printed FizzBuzz rather than Fizz or Buzz.

  2. Question 2 of 7 If the chain tested n % 3 == 0 before n % 15 == 0, what would 15 print?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: Fizz

    Order decides a chain. The most specific test, n % 15 == 0, must come first or a broader branch hides it.

  3. Question 3 of 7 A while loop's condition is false before the first turn. How many times does its body run?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: Never

    while and for test before the body and can run zero times; only do-while guarantees one run.

  4. Question 4 of 7 You want the outer loop to move to its next item as soon as an inner check fails. What do you write?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: continue with the outer loop's label, continue outer;

    A label names the loop that break leaves or continue advances. Since Dart 3.0 continue must target a loop or a switch member.

  5. Question 5 of 7 Why does a closure made with for (final x in list) capture a stable value, like a counting for loop?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: for-in binds a fresh x for each element, so each closure keeps its own

    Dart gives each turn of any for loop its own loop variable, so a closure made inside keeps that turn's value.

  6. Question 6 of 7 You must delete elements while reading a list. Which approach does not throw?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: Loop over a copy, for (final n in [...nums]), and change nums inside

    removeWhere does it in one call; when you need a loop, iterate over a copy so the list you edit is not the one you read.

  7. Question 7 of 7 Where do assertions run without any extra flag?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: In Flutter's debug mode and development-only tools that enable them by default

    asserts_session.py showed a plain dart run skipping the check. Validate real input with a throw, not an assert.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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].

  5. 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.

  6. 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

  • if and else run one block or another, while the conditional expression test ? a : b is a value you can assign; wrap every flow-control body in braces so a stray statement is never left unguarded.
  • for counts, for-in reads any Iterable (a map through its .entries), while tests before the body and do-while tests after it, so a do-while body always runs once.
  • A label before a loop lets break leave an outer loop and continue move to its next turn; since Dart 3.0 a continue must target a loop or a switch member.
  • Each turn of a for loop gets its own loop variable, so three closures made in one loop returned [0, 1, 2], where JavaScript’s shared var would give [3, 3, 3].
  • Changing a list inside its own for-in throws Concurrent modification during iteration; use removeWhere or loop over a copy, and keep assert for 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.