Control Flow in Rust
In this lesson, we'll explore how Rust handles program flow control through conditionals, loops, and pattern matching. These are essential tools for writing programs that can make decisions and repeat actions.
If Expressions
In Rust, if is an expression, which means it can return a value:
fn main() {
let number = 7;
// Basic if-else
if number < 5 {
println!("number is less than 5");
} else if number > 5 {
println!("number is greater than 5");
} else {
println!("number is 5");
}
// If in a let statement
let condition = true;
let result = if condition {
"condition was true"
} else {
"condition was false"
};
println!("Result: {}", result);
}
Loops
Rust provides several ways to repeat code:
Loop
The loop keyword creates an infinite loop that you can break out of:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Returns a value
}
};
println!("The result is {}", result); // Prints: The result is 20
}
While Loop
For conditional looping:
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
}
For Loop
For iterating over collections:
fn main() {
// Iterating over a range
for number in 1..=5 {
println!("{}!", number);
}
// Iterating over an array
let colors = ["red", "green", "blue"];
for color in colors.iter() {
println!("Color: {}", color);
}
}
Two details in those loop forms decide answers rather than style, and both are easy to skim past. A range written 1..4 stops before its upper bound while 1..=4 includes it, and break inside a loop can carry a value out the way return carries one out of a function. The program below leans on both. Work out all three numbers before you run it.
Predict
Two of these totals come from ranges that look almost identical, and the third comes from a value carried out by break. Predict all three numbers before running.
fn main() {
let mut exclusive = 0;
for n in 1..4 {
exclusive += n;
}
let mut inclusive = 0;
for n in 1..=4 {
inclusive += n;
}
let mut counter = 0;
let doubled = loop {
counter += 1;
if counter == 4 {
break counter * 2;
}
};
println!("{} {} {}", exclusive, inclusive, doubled);
}The three numbers are 6, 10, and 8. 1..4 excludes its upper bound and yields 1, 2, 3 (total 6), while 1..=4 includes it and yields 1, 2, 3, 4 (total 10) — a one-character difference and the most common off-by-one in Rust. The third comes from break counter * 2: inside a loop, break can carry an expression out as the loop's value, which is what lets loop sit on the right-hand side of a let. That value-carrying break works for loop alone, because a while or for may finish without ever reaching a break and so has no value to hand back.
Pattern Matching
One of Rust's most powerful features is pattern matching with match:
fn main() {
let number = 13;
match number {
// Match a single value
1 => println!("One!"),
// Match several values
2 | 3 | 5 | 7 | 11 | 13 => println!("This is a prime number!"),
// Match a range
14..=19 => println!("A teen"),
// Handle the rest of cases
_ => println!("Not a special number"),
}
}
If Let
For simpler pattern matching:
fn main() {
let some_value = Some(3);
// Instead of:
match some_value {
Some(3) => println!("three"),
_ => (),
}
// You can write:
if let Some(3) = some_value {
println!("three");
}
}
Practice Exercises
Try these exercises in the playground:
- Create a program that uses a loop to find the first 5 numbers divisible by both 3 and 5
- Write a function that uses pattern matching to convert numbers 1-5 to their text representation
- Use a for loop to calculate the sum of all numbers from 1 to 100
- Create a nested if-else structure and then refactor it to use match instead
Remember: Rust's control flow features are expressions, meaning they can return values. This leads to more concise and expressive code!
Try It Yourself
Reading about control flow is not the same as reaching for the right construct under your own name. This is a build task: a small program that reports its own pass/fail. Three functions are stubbed out — one branches with an if expression, one accumulates over a for range, one searches with loop and break. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each function until every check passes and it prints All checks passed.
The checks are assert_eq! calls inside main. A failing assert_eq! panics and prints both values it compared, so the first failure tells you exactly which function is still a stub and what it should have returned. Watch the boundary cases in particular: grade(10) and grade(24) are both "mild", and sum_to(4) must include the 4 — that is the inclusive-range rule from the Predict above, cashed in.
Build
Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide which control-flow construct that function needs, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.
// TODO 1: return 'cold' below 10, 'mild' from 10 through 24, and 'hot' at 25
// and above. Write it as an if / else if / else EXPRESSION, with no
// 'return' keyword and no semicolon on the branch values.
// grade(3) -> 'cold', grade(10) -> 'mild', grade(25) -> 'hot'
fn grade(celsius: i32) -> &'static str {
let _ = celsius;
""
}
// TODO 2: sum every number from 1 through last INCLUSIVE, using a for loop.
// sum_to(4) -> 10, sum_to(1) -> 1
fn sum_to(last: i32) -> i32 {
let _ = last;
0
}
// TODO 3: return the first multiple of step that is strictly greater than
// floor, using loop and break <value>.
// first_multiple_over(7, 20) -> 21, first_multiple_over(5, 5) -> 10
fn first_multiple_over(step: i32, floor: i32) -> i32 {
let _ = step;
let _ = floor;
0
}
// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
assert_eq!(grade(3), "cold", "3 degrees should grade as cold");
assert_eq!(grade(10), "mild", "10 is the FIRST mild reading, not a cold one");
assert_eq!(grade(24), "mild", "24 is the LAST mild reading");
assert_eq!(grade(25), "hot", "25 is the first hot reading");
assert_eq!(sum_to(4), 10, "sum_to(4) is 1+2+3+4 = 10 - the range must include 4");
assert_eq!(sum_to(1), 1, "sum_to(1) is just 1");
assert_eq!(first_multiple_over(7, 20), 21, "21 is the first multiple of 7 above 20");
assert_eq!(first_multiple_over(5, 5), 10, "strictly greater than 5, so 5 itself does not count");
println!("All checks passed.");
println!("grade(24) = {}", grade(24));
println!("sum_to(4) = {}", sum_to(4));
println!("first_multiple_over(7, 20) = {}", first_multiple_over(7, 20));
}Expected output: All checks passed.
grade(24) = mild
sum_to(4) = 10
first_multiple_over(7, 20) = 21
Once it passes, try two variations and predict each before running:
- Make the range exclusive. In
sum_to, change1..=lastto1..last. Predict which check fails before running. Thesum_to(4)check fails withleft: 6, right: 10— dropping the=drops the final4from the range, so the total loses exactly that one term.sum_to(1)fails too, returning0from a range that yields nothing at all. One character, two broken checks. - Put a semicolon on a branch of
grade. Change the first branch to"cold";(with a semicolon). Predict what the compiler says before running. It does not compile:error[E0308]: mismatched types, because the semicolon turns that branch into a statement whose value is()while the other branches still produce&str— and every arm of anifexpression must produce the same type. The compiler even points at the semicolon and offers to remove it.
Key Takeaways
ifin Rust is an expression — it can return a value, enablinglet x = if condition { a } else { b }- Rust has three loop types:
loop(infinite),while(conditional), andfor(iterator-based) forloops with ranges (0..10) and iterators (.iter()) are the most idiomatic way to iteratematchis exhaustive — the compiler ensures you handle every possible caseif letis syntactic sugar for matching a single pattern when you don't need fullmatch
Next Steps
With control flow under your belt, you're ready to learn about functions — how to define them, pass arguments, and return values in Rust.
Next lesson
Functions in Rust
Learn how to define Rust functions with parameters, return types, and expressions — the building blocks of every Rust program
15 min