Rust · Beginner
Control Flow in Rust: if, loop, while and for
In short: Rust branches with if, whose condition must be a bool and which can produce a value, and repeats with three loops: loop runs until break, while runs while a condition holds, and for walks through a range or a collection.
Branching and repeating
Control flow in Rust looks familiar and then differs in two ways that matter. First, the condition of an if or while must be a bool. An integer, a string or an Option is not accepted, so if count { does not compile; you write if count > 0 {. There are no parentheses around the condition, and the braces around each branch are compulsory. Second, if is an expression: it produces a value, so let fare = if age < 4 { 0 } else { 7 }; is ordinary Rust, and there is no separate ternary operator. When if is used for its value, every branch must produce the same type and an else is required, because the compiler has to know what fare is when the condition is false.
loop repeats its body until a break. Because it is guaranteed to run until you stop it, break may carry a value, and the whole loop expression evaluates to that value: let found = loop { ...; break index; };. This is the shape for retrying, polling or searching when the exit condition sits in the middle of the body. while condition { } checks its bool before every pass and stops when it is false. Rust has no do-while; write a loop with a break at the end instead.
for is the loop you will write most. It walks through anything iterable: a range, an array, a Vec, the lines of standard input. 1..=4 counts 1 to 4 inclusive; 0..4 stops before 4 and matches how indexes work, so 0..items.len() visits every valid index. (1..=4).rev() counts down. The loop variable is a fresh binding each pass, so assigning to it does not affect the iteration. There is no C-style for (i = 0; i < n; i++); a range says the same thing without an off-by-one to get wrong. Iterating for t in temps over an array visits each element by value; for t in &temps borrows them instead, which matters once the ownership rules come into play.
Inside any loop, continue jumps to the next pass and break leaves. When loops are nested, a label on the outer loop, written 'outer: for ..., lets break 'outer or continue 'outer act on it from inside the inner loop, which removes the flag variables other languages need.
match, Rust's multi-way branch, deserves its own page: see pattern matching. For a two-way or three-way decision, if and else if are the right tool.
Syntax
if hours > 8 {
println!("overtime");
} else if hours == 8 {
println!("full day");
} else {
println!("part day");
}
let rate = if hours > 8 { 15 } else { 12 }; // if as an expression
let mut n = 0;
let result = loop { // repeat until break; break can carry a value
n += 1;
if n * n > 50 { break n; }
};
while n > 0 { // repeat while the condition is true
n -= 1;
}
for i in 0..4 { } // 0, 1, 2, 3
for i in 1..=4 { } // 1, 2, 3, 4
for i in (1..=4).rev() { } // 4, 3, 2, 1
for t in [18, 21, 19] { } // each element of an array
'outer: for a in 1..4 {
for b in 1..4 {
if a * b == 6 { break 'outer; }
}
}Conditions are bare bool expressions with no parentheses; braces are never optional. .. excludes the end of a range and ..= includes it.
if as an expression, applied to each line of input
Four ages arrive on standard input; the if chain turns each into a fare, and the whole chain is assigned to fare.
use std::io::BufRead;
fn main() {
for line in std::io::stdin().lock().lines() {
let age: u32 = line.unwrap().trim().parse().unwrap();
let fare = if age < 4 {
0
} else if age < 16 {
3
} else if age >= 65 {
4
} else {
7
};
println!("Age {}: fare {}", age, fare);
}
}Input given to the program: 2 ↵ 11 ↵ 40 ↵ 70
Output
Age 2: fare 0 Age 11: fare 3 Age 40: fare 7 Age 70: fare 4
for line in std::io::stdin().lock().lines() visits every input line; each is parsed into a u32. The if chain is one expression whose value becomes fare, so there is no need to declare fare as mutable and assign in each branch. All four branches produce an integer of the same type, and the final else guarantees a value for any age. The chain is tested in order: an age of 70 is not under 16, so it reaches the age >= 65 branch.
loop with a break value, then while
The loop cannot know in advance how many passes it needs; the while runs only while more than 20 litres remain.
fn main() {
let mut litres = 0;
let mut minutes = 0;
let filled_after = loop {
litres += 7;
minutes += 1;
if litres >= 50 {
break minutes;
}
};
println!("Tank full after {} minutes ({} litres)", filled_after, litres);
let mut remaining = litres;
while remaining > 20 {
remaining -= 15;
println!("Watering... {} litres left", remaining);
}
println!("Stop: {} litres is below the limit", remaining);
}Output
Tank full after 8 minutes (56 litres) Watering... 41 litres left Watering... 26 litres left Watering... 11 litres left Stop: 11 litres is below the limit
The loop fills the tank in steps of 7 litres, so the exit test sits in the middle of the body and break minutes hands the pass count out as the value of the whole loop. The while loop checks its condition before each pass: when remaining reaches 11 the body does not run again and the final message prints. Both loops modify mut variables declared outside them.
for over ranges, an array and a labelled break
Four loops: an inclusive range, a reversed range, an index range with continue, and nested loops left with one labelled break.
fn main() {
for stop in 1..=3 {
println!("Stop {}", stop);
}
for n in (1..=3).rev() {
print!("{} ", n);
}
println!("depart");
let loads = [40, 55, 0, 62];
for i in 0..loads.len() {
if loads[i] == 0 {
continue;
}
println!("Bay {} carries {} kg", i + 1, loads[i]);
}
'shelves: for shelf in 1..=3 {
for slot in 1..=4 {
if shelf * slot > 6 {
println!("First overload at shelf {} slot {}", shelf, slot);
break 'shelves;
}
}
}
}Output
Stop 1 Stop 2 Stop 3 3 2 1 depart Bay 1 carries 40 kg Bay 2 carries 55 kg Bay 4 carries 62 kg First overload at shelf 2 slot 4
The first loop counts 1 to 3 inclusive with ..=; the second reverses a range to count down, using print! to keep the numbers on one line. The third loop uses 0..loads.len() to visit every valid index and continue to skip the empty bay. The nested loops search shelves and slots for the first product that exceeds 6; break 'shelves leaves both loops at once, so only the first hit is printed.
Common mistakes
Using a number or a string as a condition
Why it goes wrong:
if count { }is a type mismatch (E0308: expectedbool, found integer). Rust has no truthiness rule, so nothing is silently converted tobool.Fix: Write the comparison you mean:
if count > 0,if !name.is_empty().Using if as a value without an else
Why it goes wrong:
let x = if c { 1 };gives error E0317,ifmay be missing anelseclause. Withoutelsethe expression has no value when the condition is false, so it cannot be assigned.Fix: Add an
elsebranch that produces the same type, or use a plainifstatement and assign inside it.Rust · fixlet discount = if member { 10 } else { 0 };Off-by-one with .. and ..=
Why it goes wrong:
1..5yields 1 to 4; the end is excluded so that0..lenfits indexes exactly. Writing1..5when you meant to include 5 silently drops the last pass.Fix: Use
1..=5when the end value belongs in the loop, and0..items.len()for indexes.A while loop whose condition never changes
Why it goes wrong:
while remaining > 0 { println!(...) }runs forever if the body never updatesremaining. The compiler cannot detect this; the program simply never finishes.Fix: Make sure the body changes something the condition depends on, or use
forover a range when the number of passes is known.
Choosing a loop
| Loop | Use when | Can break with a value? |
|---|---|---|
loop | the exit condition is discovered inside the body, or the body must run at least once | yes |
while cond | you repeat while some state holds and the number of passes is unknown | no |
for x in iterable | you visit each item of a range or collection | no |
Where you use this
A ticket machine at a harbour reads passenger ages one per line and prints each fare, which is exactly the shape of the first example: a for over the input lines and an if expression inside. The same tool needs a retry loop when the card reader is busy: loop, try to charge, break with the receipt number on success, and give up after a fixed number of attempts with a labelled break from the inner polling loop. Every exercise on this site that says a number of lines follow on standard input is a for over lines(), and every one that asks you to count down or up is a range.
for line in std::io::stdin().lock().lines() {
let age: u32 = line.unwrap().trim().parse().unwrap();
let fare = if age < 16 { 3 } else { 7 };
println!("{}", fare);
}Key points
- An
ifcondition must be abool; there are no parentheses and braces are required. ifis an expression: with anelse, it produces a value, and all branches must have the same type.looprepeats untilbreak, andbreak valuemakes the loop evaluate to that value.whiletests aboolbefore each pass; Rust has no do-while.for x in a..bexcludesb,a..=bincludes it, and.rev()counts down.continueskips to the next pass; a label such as'outer:letsbreakorcontinuetarget an enclosing loop.
Try it yourself
The program reads a number of laps and prints it. Add a countdown that prints each lap number from laps down to 1 on its own line, using a reversed inclusive range, and then prints Go.
use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let laps: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
println!("Laps: {}", laps);
}
3Laps: 3
3
2
1
Gouse std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let laps: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
println!("Laps: {}", laps);
for n in (1..=laps).rev() {
println!("{}", n);
}
println!("Go");
}
Practise this
- Hyphenate product codesMedium
Insert a hyphen wherever a product code switches between letters and digits, for every code on input. Practise walking chars and tracking state in a Rust loop.
StringsNot started
- Library late feeEasy
Compute a library's overdue fee with a two-day grace period and a cap, using if/else and min in Rust. An exercise in simple branching.
Control flowNot started
Related lessons
- Data Types in RustRust's scalar and compound types: integer and float sizes, bool, char, tuples and arrays, plus casting with as and parsing numbers from input text.9 min
- Functions in RustHow to define and call Rust functions: typed parameters, the -> return type, returning the last expression, early return, tuples, and &str arguments.8 min
Frequently asked questions
Does Rust have a ternary operator?
No, and it does not need one. if is an expression, so let fee = if urgent { 12 } else { 5 }; does what urgent ? 12 : 5 does in C-family languages, with the same requirement that both branches have one type. For longer chains, else if or a match reads better than nested conditionals.
How do I loop over the indexes and values of an array in Rust?
For indexes alone, iterate the range 0..arr.len() and index inside the loop. For both at once, use for (i, value) in arr.iter().enumerate(), which yields pairs of an index and a reference to each element; the collections and iterators lesson covers enumerate and its relatives.
Why does Rust not have a C-style for loop?
A range expresses the same iteration more safely. for i in 0..n cannot forget to increment, cannot compare with the wrong bound, and makes the loop variable a fresh binding on every pass. When you need a different step, (0..n).step_by(2) or (0..n).rev() cover the common cases, and while handles anything irregular.
How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.