Skip to main content
Rust beginner Lesson 6 of 30

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

ConstructKey Property
if/elseExpression; both arms must have the same type
loopInfinite; can return a value via break value
while condLoops while condition is true
while let patLoops while pattern matches
for x in iterIterates any IntoIterator; most idiomatic
break / continueWorks with optional labels for nested loops
matchPattern-matching expression; must be exhaustive

Frequently Asked Questions

Is if an expression in Rust?
Yes. if/else is an expression that returns a value, so you can use it in let bindings and function returns.
What is the difference between loop and while true?
loop is the idiomatic infinite loop. It can return a value via break. The compiler also knows loop never falls through, which helps type inference.
How do I loop over a range in Rust?
Use for i in 0..n for an exclusive range or 0..=n for inclusive. You can also call .rev() to iterate in reverse.