Guide contents

Lesson 8 of 94

Control Flow: if, loop, while, for & match

Rust's control flow will feel familiar, with three twists: if and loop produce values, for loops only iterate over things (there is no C-style for (int i = 0; ...)), and match replaces switch with something far more powerful.

Concept Rust
Condition if x > 0 { ... } (no parentheses)
Conditional value if cond { a } else { b }
Infinite loop loop { ... }
Counting loop for i in 0..n
Inclusive counting loop for i in 1..=n
Labeled break 'outer: for ... { break 'outer; }
Multi-way branch match

1. if and else

Parentheses around the condition are not needed (the compiler warns if you add them), but the braces are always required, even for one line.

Rust
fn main() {
    let balance = 120;
    let price = 80;
    if balance >= price {
        println!("Purchase approved");
    } else {
        println!("Insufficient funds");
    }
}
Output
Purchase approved

2. else if chains

Rust
fn main() {
    let cpu_percent = 87;
    if cpu_percent >= 95 {
        println!("CRITICAL");
    } else if cpu_percent >= 80 {
        println!("WARNING");
    } else {
        println!("OK");
    }
}
Output
WARNING

3. if as a value

if is an expression, so you can assign its result. This replaces the ternary operator found in other languages.

Rust
fn main() {
    let items_in_cart = 1;
    let label = if items_in_cart == 1 { "item" } else { "items" };
    println!("You have {items_in_cart} {label}");
}
Output
You have 1 item

4. Both branches must have the same type

The value of an if must have one type, so every branch must produce that type.

Rust✗ does not compile · E0308
fn main() {
    let in_stock = true;
    let stock_label = if in_stock { "available" } else { 0 };
    println!("{stock_label}");
}
Output
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:3:58
  |
3 |     let stock_label = if in_stock { "available" } else { 0 };
  |                                     -----------          ^ expected `&str`, found integer
  |                                     |
  |                                     expected because of this

5. An if without else produces ()

If there is no else, the if produces () when the condition is false, so it cannot be used as a value of another type.

Rust✗ does not compile · E0317
fn main() {
    let score = 72;
    let grade = if score >= 50 { "pass" };
    println!("{grade}");
}
Output
error[E0317]: `if` may be missing an `else` clause
 --> src/main.rs:3:17
  |
3 |     let grade = if score >= 50 { "pass" };
  |                 ^^^^^^^^^^^^^^^^^------^^
  |                 |                |
  |                 |                found here
  |                 expected `&str`, found `()`
  |
  = note: `if` expressions without `else` evaluate to `()`
  = help: consider adding an `else` block that evaluates to the expected type

6. loop: repeat until break

loop repeats forever until you break. It is clearer than while (true) and the compiler understands it better.

Rust
fn main() {
    let mut attempts = 0;
    loop {
        attempts += 1;
        println!("attempt {attempts}");
        if attempts == 3 {
            break;
        }
    }
    println!("done after {attempts} attempts");
}
Output
attempt 1
attempt 2
attempt 3
done after 3 attempts

7. break with a value

A loop can return a value through break value. This is handy for "retry until it works" logic.

Rust
fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter * counter > 50 {
            break counter;
        }
    };
    println!("first number whose square exceeds 50: {result}");
}
Output
first number whose square exceeds 50: 8

8. while loops

Rust
fn main() {
    let mut remaining = 3;
    while remaining > 0 {
        println!("{remaining}...");
        remaining -= 1;
    }
    println!("Liftoff!");
}
Output
3...
2...
1...
Liftoff!

9. for over a range

0..5 is a range from 0 up to, but not including, 5. 1..=5 includes the end.

Rust
fn main() {
    for i in 0..5 {
        print!("{i} ");
    }
    println!();
    for i in 1..=5 {
        print!("{i} ");
    }
    println!();
}
Output
0 1 2 3 4
1 2 3 4 5

for loops cannot go out of bounds or loop forever by accident with a wrong i++, which is why Rust has no C-style for.

10. Counting down and stepping

.rev() reverses a range, and .step_by(n) takes every nth value.

Rust
fn main() {
    for i in (1..=5).rev() {
        print!("{i} ");
    }
    println!();
    for minute in (0..60).step_by(15) {
        print!(":{minute:02} ");
    }
    println!();
}
Output
5 4 3 2 1
:00 :15 :30 :45

11. Summing with a for loop

Rust
fn main() {
    let mut total = 0;
    for n in 1..=100 {
        total += n;
    }
    println!("1 + 2 + ... + 100 = {total}");
}
Output
1 + 2 + ... + 100 = 5050

12. continue skips to the next iteration

Rust
fn main() {
    for order_id in 1..=8 {
        if order_id % 3 == 0 {
            continue; // every third order is on hold
        }
        print!("#{order_id} ");
    }
    println!();
}
Output
#1 #2 #4 #5 #7 #8

13. Nested loops

Rust
fn main() {
    for row in 1..=4 {
        for col in 1..=4 {
            print!("{:4}", row * col);
        }
        println!();
    }
}
Output
   1   2   3   4
   2   4   6   8
   3   6   9  12
   4   8  12  16

14. Labeled break out of nested loops

Rust can label a loop and break out of it from an inner loop. The label starts with a single quote: 'outer.

Rust
fn main() {
    let target = 42;
    'search: for a in 1..=10 {
        for b in 1..=10 {
            if a * b == target {
                println!("found {a} x {b} = {target}");
                break 'search;
            }
        }
    }
    println!("search finished");
}
Output
found 6 x 7 = 42
search finished

15. Labeled continue

Rust
fn main() {
    'rows: for row in 1..=3 {
        for col in 1..=3 {
            if col > row {
                println!();
                continue 'rows;
            }
            print!("({row},{col}) ");
        }
        println!();
    }
}
Output
(1,1)
(2,1) (2,2)
(3,1) (3,2) (3,3)

16. break with a value from a labeled loop

Rust
fn main() {
    let grid_size = 5;
    let first_diagonal_multiple_of_7 = 'outer: loop {
        for i in 1..=grid_size {
            for j in 1..=grid_size {
                if i == j && (i * j) % 7 == 0 {
                    break 'outer Some(i);
                }
            }
        }
        break None;
    };
    println!("{:?}", first_diagonal_multiple_of_7);
}
Output
None

Some(i) and None are the two forms of an Option: "a value" or "no value". Here no diagonal cell is a multiple of 7, so the result is None. Option covers this in detail.

17. match: Rust's switch

match compares a value against patterns from top to bottom and runs the first arm that matches. _ matches anything. Arms never fall through, so there is no break.

Rust
fn main() {
    let day_number = 6;
    match day_number {
        1 => println!("Monday"),
        2 => println!("Tuesday"),
        3 => println!("Wednesday"),
        4 => println!("Thursday"),
        5 => println!("Friday"),
        6 | 7 => println!("Weekend"),
        _ => println!("Not a day"),
    }
}
Output
Weekend

6 | 7 matches either value.

18. match must cover every possible value

This is called exhaustiveness. The compiler checks that no value can slip through. An i32 has about four billion possible values, so without _ this does not compile.

Rust✗ does not compile · E0004
fn main() {
    let status = 2;
    match status {
        1 => println!("active"),
        2 => println!("suspended"),
    }
}
Output
error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
 --> src/main.rs:3:11
  |
3 |     match status {
  |           ^^^^^^ patterns `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
  |
  = note: the matched value is of type `i32`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms
  |
5 ~         2 => println!("suspended"),
6 ~         i32::MIN..=0_i32 | 3_i32..=i32::MAX => todo!(),
  |

Exhaustiveness becomes very powerful with enums. When you add a new enum variant, every match that forgets it stops compiling. See Enums.

19. match produces a value

Rust
fn main() {
    let http_status = 404;
    let message = match http_status {
        200 => "OK",
        301 => "Moved Permanently",
        404 => "Not Found",
        500 => "Internal Server Error",
        _ => "Unknown",
    };
    println!("{http_status} {message}");
}
Output
404 Not Found

20. Matching ranges

a..=b in a pattern matches every value from a to b inclusive.

Rust
fn main() {
    let age = 34;
    let group = match age {
        0..=12 => "child",
        13..=17 => "teenager",
        18..=64 => "adult",
        _ => "senior",
    };
    println!("{age} is {group}");
}
Output
34 is adult

21. Match guards

An if after a pattern adds an extra condition, called a guard. The pattern n matches any value and names it n.

Rust
fn main() {
    for balance in [-50, 0, 20, 5_000] {
        let status = match balance {
            n if n < 0 => "overdrawn",
            0 => "empty",
            n if n < 100 => "low",
            _ => "healthy",
        };
        println!("{balance:>5}: {status}");
    }
}
Output
  -50: overdrawn
    0: empty
   20: low
 5000: healthy

[-50, 0, 20, 5_000] is an array. A for loop can go through it just like a range. Arrays are the topic of the next lesson.

22. Realistic example: FizzBuzz with match

Matching on two values at once, (n % 3, n % 5), makes the four cases very clear. The pair is a tuple, covered in the next lesson.

Rust
fn main() {
    for n in 1..=15 {
        match (n % 3, n % 5) {
            (0, 0) => println!("FizzBuzz"),
            (0, _) => println!("Fizz"),
            (_, 0) => println!("Buzz"),
            _ => println!("{n}"),
        }
    }
}
Output
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

23. Realistic example: finding prime numbers

Rust
fn main() {
    let mut primes_found = 0;
    'candidates: for n in 2..=50 {
        let mut divisor = 2;
        while divisor * divisor <= n {
            if n % divisor == 0 {
                continue 'candidates;
            }
            divisor += 1;
        }
        print!("{n} ");
        primes_found += 1;
    }
    println!();
    println!("{primes_found} primes up to 50");
}
Output
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
15 primes up to 50

24. Realistic example: retry with exponential backoff

This simulates a flaky service that succeeds on the fourth call. loop with break value returns the result, and the delay doubles each time up to a limit.

Rust
const MAX_ATTEMPTS: u32 = 5;
const MAX_DELAY_MS: u32 = 1_000;

fn call_service(attempt: u32) -> bool {
    attempt >= 4 // pretend the service recovers on the 4th try
}

fn main() {
    let mut attempt = 0;
    let mut delay_ms = 100;
    let succeeded = loop {
        attempt += 1;
        if call_service(attempt) {
            break true;
        }
        if attempt == MAX_ATTEMPTS {
            break false;
        }
        println!("attempt {attempt} failed, retrying in {delay_ms} ms");
        delay_ms = (delay_ms * 2).min(MAX_DELAY_MS);
    };
    println!("succeeded: {succeeded} after {attempt} attempt(s)");
}
Output
attempt 1 failed, retrying in 100 ms
attempt 2 failed, retrying in 200 ms
attempt 3 failed, retrying in 400 ms
succeeded: true after 4 attempt(s)

25. Realistic example: savings growth with while

How many years until a deposit doubles at 7% interest per year?

Rust
fn main() {
    let start = 10_000.0;
    let rate = 0.07;
    let mut balance = start;
    let mut years = 0;
    while balance < start * 2.0 {
        balance *= 1.0 + rate;
        years += 1;
        if years % 3 == 0 {
            println!("year {years:2}: {balance:.2}");
        }
    }
    println!("Doubled after {years} years: {balance:.2}");
}
Output
year  3: 12250.43
year  6: 15007.30
year  9: 18384.59
Doubled after 11 years: 21048.52

26. Realistic example: the Collatz sequence

Each step halves an even number or computes 3n + 1 for an odd one. The if expression picks the next value.

Rust
fn main() {
    let mut n: u64 = 27;
    let mut steps = 0;
    let mut highest = n;
    while n != 1 {
        n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
        highest = highest.max(n);
        steps += 1;
    }
    println!("27 reaches 1 after {steps} steps, peaking at {highest}");
}
Output
27 reaches 1 after 111 steps, peaking at 9232

27. Realistic example: classifying HTTP responses

A web client often decides what to do based on the status code's range. The match covers every possible u16 value, so the compiler guarantees that no status is forgotten.

Rust
fn action_for(status: u16) -> &'static str {
    match status {
        100..=199 => "informational: keep waiting",
        200 | 201 | 204 => "success: process body",
        200..=299 => "success: unusual code, log it",
        301 | 302 | 307 | 308 => "redirect: follow Location header",
        300..=399 => "redirect: unsupported",
        401 | 403 => "auth problem: refresh token",
        429 => "rate limited: back off",
        400..=499 => "client error: do not retry",
        500..=599 => "server error: retry later",
        _ => "invalid status code",
    }
}

fn main() {
    for status in [200, 206, 302, 401, 429, 404, 503, 999] {
        println!("{status}: {}", action_for(status));
    }
}
Output
200: success: process body
206: success: unusual code, log it
302: redirect: follow Location header
401: auth problem: refresh token
429: rate limited: back off
404: client error: do not retry
503: server error: retry later
999: invalid status code

Arms are checked from top to bottom, so 200 | 201 | 204 wins over the wider 200..=299 that follows it.

Summary

  • if needs no parentheses but always needs braces. It is an expression, so it replaces the ternary operator.
  • loop repeats until break, and break value returns a value from it.
  • while repeats while a condition holds. for iterates over ranges (0..n, 1..=n) and collections.
  • Labels ('outer) let break and continue target an outer loop.
  • match replaces switch: no fall-through, patterns with |, ranges, and guards, and the compiler checks that every case is covered.

Next: Tuples & Arrays.