Beginner

Control Flow in Rust

Learn conditionals, match expressions, and loops in Rust with practical Docker-ready examples covering if/else, match, loop, while, and for

Control flow is how a program decides what to do next—branching on conditions and repeating work. Rust gives you the familiar tools (if, while, for) but with a twist that reflects its multi-paradigm roots: in Rust, if and match are expressions, not just statements. That means they produce values you can bind to variables, which leads to concise, assignment-free code.

Rust also leans on its functional heritage with match, an exhaustive pattern-matching construct far more powerful than a C-style switch. The compiler forces you to handle every possible case, eliminating a whole class of “forgot a branch” bugs at compile time. Combined with strong static typing, this makes Rust’s control flow both expressive and safe.

In this tutorial you’ll learn how to branch with if/else, use if as an expression, pattern-match with match, and iterate with Rust’s three loop forms: loop, while, and for. You’ll also see loop control with break and continue, including the unique ability of loop to return a value.

Conditionals with if / else

The basic conditional looks like other C-family languages, but note there are no parentheses around the condition, and the body braces are required even for a single line.

Create a file named conditionals.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn main() {
    let number = 7;

    // if / else if / else chain
    if number < 5 {
        println!("{} is less than 5", number);
    } else if number == 5 {
        println!("{} is exactly 5", number);
    } else {
        println!("{} is greater than 5", number);
    }

    // if is an EXPRESSION — it returns a value
    let parity = if number % 2 == 0 { "even" } else { "odd" };
    println!("{} is {}", number, parity);
}

Because if is an expression, the second example assigns its result directly to parity. Both branches must return the same type (here, &str), and the compiler enforces that. This replaces the ternary operator (?:) that Rust deliberately omits.

Pattern Matching with match

match is Rust’s most powerful branching tool. It compares a value against a series of patterns and runs the first that fits. Crucially, match must be exhaustive—every possible value has to be covered, often via the _ wildcard. Like if, match is an expression.

Create a file named matching.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let number = 7;

    // match returns a value; arms can use ranges and a wildcard
    let category = match number {
        0 => "zero",
        1..=5 => "low",
        6..=9 => "high",
        _ => "out of range",
    };
    println!("{} is in category: {}", number, category);

    // match on a boolean-like signal
    let signal = "yellow";
    match signal {
        "red" => println!("Stop"),
        "yellow" => println!("Slow down"),
        "green" => println!("Go"),
        other => println!("Unknown signal: {}", other),
    }
}

The 1..=5 syntax is an inclusive range pattern. The final arm other binds the unmatched value to a name so you can use it—a common alternative to the bare _ wildcard when you want the value.

Loops: loop, while, and for

Rust has three loop keywords. loop runs forever until you break; while runs as long as a condition holds; and for iterates over a range or collection. A standout feature: break inside a loop can return a value.

Create a file named loops.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
    // 1. loop with break returning a value
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 5 {
            break counter * 10; // break can carry a value out
        }
    };
    println!("loop result: {}", result);

    // 2. while loop
    let mut n = 3;
    while n > 0 {
        println!("while countdown: {}", n);
        n -= 1;
    }

    // 3. for over an inclusive range
    for i in 1..=3 {
        println!("for range: {}", i);
    }
}

Note let mut—loop counters must be declared mutable, since Rust variables are immutable by default. The for i in 1..=3 form is the idiomatic way to count; Rust has no C-style for (i = 0; i < n; i++) loop because ranges and iterators cover those cases more safely.

Loop Control with break and continue

break exits a loop early and continue skips to the next iteration—the same as most languages. Rust adds loop labels (written 'name:) so you can break out of an outer loop from within an inner one.

Create a file named loop_control.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let nums = [10, 20, 30, 40];

    // continue skips the matching element
    for value in nums {
        if value == 20 {
            continue;
        }
        println!("for item: {}", value);
    }

    // labeled loops: break the OUTER loop from inside the inner one
    'outer: for x in 1..=3 {
        for y in 1..=3 {
            if x * y >= 4 {
                println!("stopping at x={}, y={}", x, y);
                break 'outer;
            }
        }
    }
}

The for value in nums loop iterates the array by value (each i32 is copied). The labeled 'outer break jumps all the way out of both loops at once—without a label, break would only exit the inner loop.

Running with Docker

Compile and run each example using the official Rust image. The rustc compiler produces an executable named after the source file (minus the .rs extension).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull rust:1.83

# Compile and run the conditionals example
docker run --rm -v $(pwd):/app -w /app rust:1.83 sh -c 'rustc conditionals.rs && ./conditionals'

# Compile and run the match example
docker run --rm -v $(pwd):/app -w /app rust:1.83 sh -c 'rustc matching.rs && ./matching'

# Compile and run the loops example
docker run --rm -v $(pwd):/app -w /app rust:1.83 sh -c 'rustc loops.rs && ./loops'

# Compile and run the loop control example
docker run --rm -v $(pwd):/app -w /app rust:1.83 sh -c 'rustc loop_control.rs && ./loop_control'

Expected Output

Running conditionals.rs:

7 is greater than 5
7 is odd

Running matching.rs:

7 is in category: high
Slow down

Running loops.rs:

loop result: 50
while countdown: 3
while countdown: 2
while countdown: 1
for range: 1
for range: 2
for range: 3

Running loop_control.rs:

for item: 10
for item: 30
for item: 40
stopping at x=2, y=2

Key Concepts

  • if is an expression — It returns a value, so you can write let x = if cond { a } else { b }. Both branches must produce the same type. Rust has no ternary operator because it doesn’t need one.
  • match is exhaustive — The compiler requires every possible case to be handled, catching missing branches at compile time. Use _ or a binding name for the catch-all arm.
  • Range patterns1..=5 matches inclusively in match arms and drives for loops, replacing manual index counters.
  • Three loop formsloop (infinite until break), while (condition-controlled), and for (iterates ranges/collections). There is no C-style for(;;) loop.
  • break can return a value — Only loop supports break value, letting a loop compute and hand back a result.
  • Loop labels — Annotate loops with 'label: and use break 'label or continue 'label to control outer loops from within nested ones.
  • Mutability is explicit — Loop counters and accumulators need let mut, since bindings are immutable by default.

Running Today

All examples can be run using Docker:

docker pull rust:1.83
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining