Control Flow in Rust
Master if/else expressions, loop, while, for, ranges, and breaking with values in Rust.
if / else as an Expression
Unlike C or Java, if/else in Rust is an expression — it produces a value that can be assigned to a variable or returned from a function. This eliminates the need for a separate ternary operator and makes code more compositional. Both arms of an if expression must produce the same type; mismatched types are a compile error.
fn main() {
let number = 7;
// Statement form — used for side effects
if number < 10 {
println!("single digit");
} else if number < 100 {
println!("double digit");
} else {
println!("three or more digits");
}
// Expression form — both arms produce the same type (&str here)
let description = if number % 2 == 0 { "even" } else { "odd" };
println!("{} is {}", number, description);
// Used directly in a let binding — replaces the ternary operator
let abs = if number >= 0 { number } else { -number };
println!("abs = {}", abs);
}
loop — Infinite Loop
loop runs forever until an explicit break. It is the idiomatic way to express “keep trying until a condition is met” — for example, retrying an operation or polling for input. The key advantage over while true is that loop can return a value, and the compiler recognises it as a type that never naturally falls through, which helps with type inference.
fn main() {
let mut counter = 0;
loop {
counter += 1;
if counter == 5 {
break; // exit the loop
}
}
println!("counter = {}", counter); // 5
}
Returning a Value from loop
Pass a value to break to return it from the loop expression. This is useful for retry loops where you want to capture the result of the successful attempt.
fn main() {
let mut attempt = 0;
// The result of the loop expression is whatever value was passed to break
let result = loop {
attempt += 1;
if attempt * attempt > 50 {
break attempt; // break with a value — this becomes the loop expression's value
}
};
println!("first n where n² > 50: {}", result); // 8
}
Loop Labels
Labels ('label:) let you break or continue an outer loop from within a nested loop, which is cleaner than managing boolean flags to signal early exit.
fn main() {
'outer: for x in 0..5 {
for y in 0..5 {
if x + y == 6 {
println!("breaking outer at x={}, y={}", x, y);
break 'outer; // exits the outer loop entirely
}
}
}
}
// Output: breaking outer at x=2, y=4
while Loop
while executes its body as long as a condition is true. It is the right tool when the number of iterations is not known upfront but the termination condition is a simple boolean expression.
fn main() {
let mut n = 1;
while n < 100 {
n *= 2; // keep doubling
}
println!("first power of 2 >= 100: {}", n); // 128
}
while let is a pattern-matching variant that loops as long as a destructuring pattern matches. It is the idiomatic way to consume an iterator or drain a stack:
fn main() {
let mut stack = vec![1, 2, 3];
// Loops until stack.pop() returns None (when the vec is empty)
while let Some(top) = stack.pop() {
println!("{}", top); // 3, 2, 1
}
}
for Loop and Iterators
for iterates over anything that implements the IntoIterator trait — arrays, vectors, ranges, hash maps, and more. It is the most idiomatic loop in Rust because it removes the possibility of off-by-one errors and out-of-bounds indexing entirely. You never manage an index manually.
fn main() {
let fruits = ["apple", "banana", "cherry"];
// &fruits borrows the array — fruits is still usable after the loop
for fruit in &fruits {
println!("{}", fruit);
}
}
Iterating with Index
When you need both the element and its position, use .enumerate(). This is safer and more idiomatic than maintaining a separate counter variable.
fn main() {
let words = ["zero", "one", "two", "three"];
// enumerate() yields (index, &element) tuples
for (i, word) in words.iter().enumerate() {
println!("{}: {}", i, word);
}
}
Ranges
Ranges give you a concise way to iterate over a sequence of numbers without allocating anything. The exclusive form 0..n is most common; the inclusive form 0..=n is used when the upper bound should be included.
fn main() {
// Exclusive range: produces 0, 1, 2, 3, 4
for i in 0..5 {
print!("{} ", i);
}
println!();
// Inclusive range: produces 0, 1, 2, 3, 4, 5
for i in 0..=5 {
print!("{} ", i);
}
println!();
// Reverse iteration
for i in (0..5).rev() {
print!("{} ", i); // 4 3 2 1 0
}
println!();
// Step by a custom amount
for i in (0..20).step_by(3) {
print!("{} ", i); // 0 3 6 9 12 15 18
}
println!();
}
Consuming vs. Borrowing
How you write the for loop determines whether the collection is borrowed (left intact) or consumed (moved into the loop):
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Borrow with &numbers — the vec is still usable after the loop
for n in &numbers {
print!("{} ", n);
}
println!();
println!("still have: {:?}", numbers); // works fine
// Consume — numbers is moved into the loop and dropped when it ends
for n in numbers {
print!("{} ", n);
}
// println!("{:?}", numbers); // ERROR: value moved into the for loop
}
continue — Skip to Next Iteration
continue skips the remainder of the current loop body and jumps to the next iteration. Like break, it can be used with labels to affect an outer loop.
fn main() {
for i in 0..10 {
if i % 2 == 0 {
continue; // skip even numbers — jump straight to the next i
}
print!("{} ", i); // 1 3 5 7 9
}
println!();
}
match as Control Flow
match is a powerful expression for branching on values. It is exhaustive — the compiler requires every possible value to be handled. Full coverage is in the pattern matching tutorial; here is a quick look at how it reads as control flow:
fn classify(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
1..=9 => "single digit",
_ => "large", // _ catches everything else
}
}
fn main() {
println!("{}", classify(-5)); // negative
println!("{}", classify(0)); // zero
println!("{}", classify(7)); // single digit
println!("{}", classify(100)); // large
}
Practical Example: FizzBuzz
This classic problem demonstrates combining multiple control flow features in a concise, idiomatic way. The match on a tuple of remainders is a particularly clean Rust solution.
fn fizzbuzz(n: u32) -> String {
// Match on a tuple of both remainders simultaneously
match (n % 3, n % 5) {
(0, 0) => String::from("FizzBuzz"), // divisible by both
(0, _) => String::from("Fizz"), // divisible by 3 only
(_, 0) => String::from("Buzz"), // divisible by 5 only
_ => n.to_string(), // not divisible by either
}
}
fn main() {
for i in 1..=20 {
println!("{}", fizzbuzz(i));
}
}
Summary
| Construct | Key Property |
|---|---|
if/else | Expression; both arms must have the same type |
loop | Infinite; can return a value via break value |
while cond | Loops while condition is true |
while let pat | Loops while pattern matches |
for x in iter | Iterates any IntoIterator; most idiomatic |
break / continue | Works with optional labels for nested loops |
match | Pattern-matching expression; must be exhaustive |