Conditions, loops and labels

Control flow in Rust is made of expressions, not statements. An if produces a value, a loop can produce a value through break, and a block produces the value of its last expression. That is why a condition can be assigned to a binding without a temporary variable.

The three loops have different jobs: loop repeats until a break, while repeats while a condition holds, and for walks an iterator. The lesson ends with labels, the one place where a nested loop needs to say which loop it is leaving.

if is an expression

The condition must be a bool; there is no truthiness, so an integer is not a condition and the compiler says so. else if chains as in other languages, and the whole chain is one expression whose value is the value of the branch that ran.

The program below assigns the result of the chain to advice. All three branches are string slices, so the value has one type and the binding needs no annotation. A branch that ends without an expression produces (), which the next section turns into an error.

fn main() { let temperature = 18; let advice = if temperature > 25 { "shorts" } else if temperature > 15 { "jacket" } else { "coat" }; println!("{temperature} degrees: {advice}"); }

rustc --edition 2024 branches.rs
./branches

18 degrees: jacket

Eighteen degrees is above fifteen and below twenty-six, so the middle branch ran. The chain is also the place where a missing else shows up: the compiler then needs the branch value to be (), because there is nothing to produce otherwise.

Branches that disagree

Every branch of an if used as a value must produce the same type. One branch returning an integer and another a string literal is E0308, and the message names the two types and points at both branches.

The error is worth seeing because it is the same rule that governs match arms and the value of a block: an expression has one type, decided before the program runs, even when only one branch will execute.

fn main() { let value = if true { 1 } else { "two" }; println!("{value}"); }

rustc --edition 2024 branch_types.rs

error[E0308]: `if` and `else` have incompatible types
--> branch_types.rs:2:38
|
2 | let value = if true { 1 } else { "two" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

The note says which type the compiler took as the expected one, and it is the first branch that set it. Fixing the mismatch usually means converting one side, and the lesson on formatting shows how to turn both sides into a String when that is the honest answer.

loop that returns a value

loop repeats its body forever until a break. The break may carry a value, and then the loop itself is an expression of that type. That is how a search that does not know how many steps it needs returns its result without an extra mutable binding.

The program squares an increasing counter and stops at the first square above fifty. The value of the loop is the counter at that moment, eight, and the code after the loop uses it as an ordinary number.

fn main() { let mut count = 0; let found = loop { count += 1; if count * count > 50 { break count; } }; println!("first above 50: {found}, squared: {}", found * found); }

rustc --edition 2024 loop_value.rs
./loop_value

first above 50: 8, squared: 64

A loop without a break has the type !, the never type, which is compatible with every other type. That is why loop {} can stand where a value is expected, and why code after it is unreachable.

while

while checks a condition before each pass and stops when it becomes false. The condition is a bool expression, and the loop produces no value; anything it computes has to live in a binding outside it.

The countdown below is the standard shape: a mutable counter, a condition on it, and an update at the end of the body. A for over a range would be shorter, and the next section shows when that is the better choice.

fn main() { let mut n = 5; while n > 0 { println!("{n}"); n -= 1; } println!("liftoff"); }

rustc --edition 2024 countdown.rs
./countdown

5
4
3
2
1
liftoff

Nothing prints after zero because the condition is checked first. A while true with a break inside is the same as loop, and rustc accepts both; the loop form is preferred because it says that the exit is inside the body.

for and ranges

for takes an iterator and runs the body once per item. A range is the simplest one: 0..5 stops before five and 0..=5 includes it. The body can destructure the item, so enumerate gives a pair of index and value and the pattern unpacks it in the header.

The program walks a range and then an array with its indices. Iterating over an array by value copies the elements here, which is cheap for strings; for a vector of large structs the same code would need references, and the lesson on iterators explains the difference.

fn main() { for i in 1..=3 { println!("i: {i}"); } let names = ["ada", "grace", "alan"]; for (index, name) in names.iter().enumerate() { println!("{index}: {name}"); } }

rustc --edition 2024 ranges.rs
./ranges

i: 1
i: 2
i: 3
0: ada
1: grace
2: alan

The index starts at zero because enumerate counts from zero, and the range prints one, two, three because it is inclusive. Off-by-one mistakes are still possible, but the two forms make the choice visible in the source.

Labels on nested loops

A label is a name with a leading quote, and break 'outer or continue 'outer acts on the loop that carries it. Without a label a break leaves only the innermost loop, which is rarely what a search over two dimensions wants.

The program looks for the first pair whose product is greater than six and leaves both loops at once. The counter outside the loops shows how many pairs were visited before the stop, which is the evidence that the outer loop really ended early.

fn main() { let mut pairs = 0; 'outer: for a in 0..5 { for b in 0..5 { if a * b > 6 { println!("stop at {a},{b}"); break 'outer; } pairs += 1; } } println!("pairs: {pairs}"); }

rustc --edition 2024 labels.rs
./labels

stop at 2,4
pairs: 14

The label is also allowed on a plain block, where break 'name value produces the block value. That is a way to leave a long chain of statements early without a function, and the lesson on functions compares it with an early return.

A worked example

FizzBuzz is the smallest program that uses a loop, a chain of conditions and a remainder. The order of the checks matters: fifteen is divisible by both three and five, so the combined case has to come first, and the compiler does not warn about a branch that can never run here because it cannot know the arithmetic.

The output below is the whole run for one to fifteen. It is a good first exercise because every mistake is visible in the output rather than in a message.

fn main() { for n in 1..=15 { if n % 15 == 0 { println!("FizzBuzz"); } else if n % 3 == 0 { println!("Fizz"); } else if n % 5 == 0 { println!("Buzz"); } else { println!("{n}"); } } }

rustc --edition 2024 fizzbuzz.rs
./fizzbuzz

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

A match over the pair of remainders is the shorter version, and the lesson on pattern matching rewrites exactly this program that way.

Exercises

  1. Rewrite the countdown with a for over a reversed range and compare the two versions. Which one says "five times" more clearly?
  2. Change the label example so it continues the outer loop instead of breaking it. How many pairs are visited now?
  3. Print the FizzBuzz numbers up to thirty and check the two multiples of fifteen. What would the program print for zero, and why?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is the type of the value produced by loop { break 42; }?

    Answer

    The integer type of the literal, here i32

    break with a value makes the loop an expression of that value, and 42 defaults to i32.

  2. What type must the branches of an if used as a value have?

    Answer

    The same type in every branch

    The expression has one type, decided at compile time, so every branch must produce it.

  3. What does break 'outer; do inside a nested loop?

    Answer

    It breaks the loop that carries the label

    A labeled break leaves the loop with that label, however many loops are inside it.

  4. Which numbers does for i in 0..3 visit?

    Answer

    0, 1, 2

    A range with two dots excludes its end, and 0..=3 would include three.

  5. What does this program print? fn main() { let mut n = 2; while n > 0 { println!("{n}"); n -= 1; } println!("done"); }

    Answer

    2 / 1 / done

    The loop runs for two and one, and the condition stops it before zero.

  6. What does this program print? fn main() { let mut count = 0; let found = loop { count += 1; if count * count > 20 { break count; } }; println!("{count} {found}"); }

    Answer

    5 5

    Four squared is sixteen and five squared is twenty-five, so the loop leaves at five.

  7. What does this program print? fn main() { let names = ["ada", "alan"]; for (index, name) in names.iter().enumerate() { println!("{index}: {name}"); } }

    Answer

    0: ada / 1: alan

    enumerate pairs each item with a counter that starts at zero.

  8. What is the first line of the compiler output for this program? fn main() { let value = if true { 1 } else { "two" }; println!("{value}"); }

    Answer

    error[E0308]: if and else have incompatible types

    The two branches disagree, and the specialized message names the if and the else.

  9. What does this program print? fn main() { let mut n = 3; while n > 0 { println!("{n}"); n -= 1; } println!("liftoff"); }

    Answer

    3 / 2 / 1 / liftoff

    The condition is checked before each pass, so three, two and one print and zero does not.

  10. What does this program print? fn main() { let mut pairs = 0; 'outer: for a in 0..3 { for b in 0..3 { if a * b > 2 { println!("stop at {a},{b}"); break 'outer; } pairs += 1; } } println!("pairs: {pairs}"); }

    Answer

    stop at 2,2 / pairs: 8

    The first product above two is at a equal to two and b equal to two, after eight counted pairs.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev