Understanding Control Flow in Rust
Control flow determines which parts of a program run and how often they run. Rust provides expressive control-flow constructs including if, match, loop, while, and for. Understanding these tools is essential for writing programs that make decisions, repeat work, and handle different states safely.
What Is Control Flow?
Control flow describes the order in which statements and expressions are evaluated. It lets a program choose between branches, repeat operations, and respond differently to values or conditions.
1. Using if in Rust
if executes a block when a boolean condition is true:
fn main() {
let number = 10;
if number > 5 {
println!("The number is greater than 5.");
} else {
println!("The number is 5 or less.");
}
}Important points:
- An
ifcondition must evaluate tobool. - Use
elsefor an alternative branch. - Chain conditions with
else ifwhen needed.
fn main() {
let score = 85;
if score >= 90 {
println!("Grade: A");
} else if score >= 80 {
println!("Grade: B");
} else {
println!("Grade: C or lower");
}
}Rust’s if is also an expression, so it can return a value as long as each branch has a compatible type.
2. Using match
match compares a value against patterns and evaluates the matching arm:
fn main() {
let day = "Monday";
match day {
"Monday" => println!("The work week begins."),
"Friday" => println!("The weekend is almost here!"),
"Saturday" | "Sunday" => println!("It is the weekend!"),
_ => println!("A regular weekday."),
}
}Important points:
matchcompares an expression against patterns._is a catch-all pattern.- A
matchmust be exhaustive, meaning every possible value is covered.
3. Loops in Rust
Rust provides loop, while, and for.
loop
loop repeats indefinitely until execution leaves it, often with break:
fn main() {
let mut count = 0;
loop {
count += 1;
if count == 5 {
break;
}
println!("Count: {}", count);
}
}A loop can also return a value with break value.
while
while repeats while its condition remains true:
fn main() {
let mut number = 3;
while number != 0 {
println!("Number: {}", number);
number -= 1;
}
println!("Lift off!");
}for
for iterates over an iterator, collection, or range:
fn main() {
let fruits = ["apple", "banana", "cherry"];
for fruit in &fruits {
println!("Fruit: {}", fruit);
}
for i in 1..5 {
println!("Number: {}", i);
}
}Important points:
foris usually the clearest choice for iterating over collections.1..5produces the values 1 through 4.- Borrow a collection when you want to iterate without consuming it.
4. Control Flow with Result and Option
Pattern matching is frequently used with Result and Option:
fn main() {
let result: Result<i32, &str> = Ok(10);
match result {
Ok(value) => println!("Success: {}", value),
Err(error) => println!("Error: {}", error),
}
match divide(10, 2) {
Some(value) => println!("Result: {}", value),
None => println!("Cannot divide by zero."),
}
}
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}This makes possible and error states explicit rather than relying on null values or unchecked exceptions.
Best Practices
- Use
matchwhen pattern-based branching makes the cases clearer than nested conditionals. - Make sure intentionally infinite loops have a clear exit strategy when appropriate.
- Keep
matcharms exhaustive and use_deliberately rather than hiding meaningful cases. - Prefer
forwhen iterating over collections or ranges. - Use
if let,while let, combinators, or the?operator when they express a simpleOptionorResultflow more clearly than a fullmatch.
Conclusion
Rust’s control-flow constructs combine familiar branching and looping with powerful pattern matching. By choosing the construct that best represents the program’s states and transitions, you can write code that is easier to read, safer to change, and explicit about exceptional cases.