ZetCode

Rust control flow

last modified October 25, 2026

In this article we show how to control the flow of a Rust program.

The statements of a Rust program are executed from top to bottom. The flow of the program can be altered with conditions and loops: if, else, loop, while, for, and match. The break and continue keywords change the flow inside loops.

Most of these constructs are expressions in Rust; they can produce a value that we assign to a variable.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q. For generating random values, the first two examples use the rand crate.

Cargo.toml
[dependencies]
rand = "0.10.3"

We add the dependency with cargo add rand.

The if condition

The if keyword creates a conditional test. It can be followed by else if and else branches. The condition must be a bool; unlike in C, an integer is not accepted. The condition is not placed in parentheses, but the braces around the blocks are required.

main.rs
fn main() {
    let n = rand::random_range(-5..5);

    if n < 0 {
        println!("{n} is negative");
    } else if n == 0 {
        println!("{n} is zero");
    } else {
        println!("{n} is positive");
    }
}

We randomly pick a value from the range -5..5. Based on the value, we print a message. There are three branches that can be executed.

let n = rand::random_range(-5..5);

The random_range function returns a random value from the given range. The range -5..5 includes -5 but excludes 5.

if n < 0 {
    println!("{n} is negative");
} else if n == 0 {

If the condition after if is true, its block is executed and the other branches are skipped. Otherwise, the else if condition is tested.

} else {
    println!("{n} is positive");
}

The else block is executed when none of the previous conditions is true.

$ cargo run -q
2 is positive
$ cargo run -q
1 is positive
$ cargo run -q
-3 is negative

The if expression

The if is an expression; it can return a value. Rust has no ternary operator, the if expression is used instead.

main.rs
fn main() {
    let n = rand::random_range(-5..5);

    let msg = if n < 0 {
        "negative"
    } else if n == 0 {
        "zero"
    } else {
        "positive"
    };

    println!("{n} is {msg}");
}

The if expression returns a string slice, which we assign to the msg variable.

let msg = if n < 0 {
    "negative"
} else if n == 0 {

The value of each branch is the last expression of its block, written without a semicolon. All branches must return the same type. The whole let statement ends with a semicolon after the closing brace.

$ cargo run -q
-4 is negative
$ cargo run -q
2 is positive
$ cargo run -q
4 is positive

The loop keyword

An infinite loop is created with the loop keyword. The loop is terminated with the break keyword.

main.rs
fn main() {
    let mut i = 0;

    loop {
        println!("falcon");

        i += 1;
        if i == 5 {
            break;
        }
    }
}

In the example, we print a word five times.

let mut i = 0;

We define a counter. It must be mutable, since we change it in the loop.

i += 1;
if i == 5 {
    break;
}

In each iteration, we increase the counter. When it reaches 5, the loop is terminated with break.

$ cargo run -q
falcon
falcon
falcon
falcon
falcon

Returning a value from loop

A loop is an expression too. The value given to break becomes the value of the loop.

main.rs
fn main() {
    let mut a = 0u64;
    let mut b = 1u64;

    let first_big = loop {
        (a, b) = (b, a + b);

        if a > 1000 {
            break a;
        }
    };

    println!("The first Fibonacci number above 1000 is {first_big}");
}

The program generates Fibonacci numbers until it finds the first one greater than 1000.

(a, b) = (b, a + b);

With a destructuring assignment, we move to the next pair of Fibonacci numbers in one step.

if a > 1000 {
    break a;
}

The break a statement ends the loop and makes a the value of the whole loop expression, which is assigned to first_big.

$ cargo run -q
The first Fibonacci number above 1000 is 1597

The while loop

The while loop executes its block as long as the condition is true. The condition is checked before each iteration.

main.rs
fn main() {
    let mut x = 1;

    while x <= 10 {
        print!("{x} ");
        x += 1;
    }

    println!();
    println!("x: {x}");
}

In the program, we print the numbers 1 through 10. In the end, we print the value of the x counter.

while x <= 10 {
    print!("{x} ");
    x += 1;
}

The block is executed while x is less than or equal to 10. When x becomes 11, the condition is false and the loop ends; that is why the last line shows 11.

$ cargo run -q
1 2 3 4 5 6 7 8 9 10 
x: 11

The while loop is useful when we do not know the number of iterations in advance. In the next example, we compute the Collatz sequence.

main.rs
fn main() {
    let mut n = 27;
    let mut steps = 0;

    while n != 1 {
        n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
        steps += 1;
    }

    println!("27 reaches 1 after {steps} steps");
}

If the number is even, we divide it by two; otherwise, we multiply it by three and add one. We repeat this until the number reaches 1 and count the steps.

n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };

The new value of n is computed with an if expression.

$ cargo run -q
27 reaches 1 after 111 steps

The for loop

The for loop goes over the values of an iterator, such as a range or a collection. It is the most common loop in Rust.

main.rs
fn main() {
    for i in 1..=10 {
        print!("{i} ");
    }
    println!();

    for i in (0..20).step_by(5) {
        print!("{i} ");
    }
    println!();

    for i in (1..=5).rev() {
        print!("{i} ");
    }
    println!();

    let words = ["sky", "cup", "forest"];

    for (i, word) in words.iter().enumerate() {
        println!("{i}: {word}");
    }
}

In the program, we iterate over ranges and an array.

for i in 1..=10 {
    print!("{i} ");
}

The inclusive range 1..=10 includes 10; the range 1..10 would stop at 9. In each iteration, the i variable holds the current value.

for i in (0..20).step_by(5) {

The step_by method takes every fifth value of the range.

for i in (1..=5).rev() {

With rev, we count down from 5 to 1.

for (i, word) in words.iter().enumerate() {
    println!("{i}: {word}");
}

The enumerate method gives us the index together with each element of the array.

$ cargo run -q
1 2 3 4 5 6 7 8 9 10 
0 5 10 15 
5 4 3 2 1 
0: sky
1: cup
2: forest

The continue keyword

The continue keyword skips the rest of the current iteration and starts the next one.

main.rs
fn main() {
    for i in 1..=20 {
        if i % 3 != 0 {
            continue;
        }

        print!("{i} ");
    }

    println!();
}

We print the numbers from 1 to 20 that are divisible by three. For other numbers, continue skips the print! call.

$ cargo run -q
3 6 9 12 15 18 

Loop labels

By default, break and continue apply to the innermost loop. With a label, we can break out of an outer loop. A label starts with a single quote.

main.rs
fn main() {
    let grid = [[1, 4, 7], [2, 9, 3], [8, 5, 6]];

    'rows: for (r, row) in grid.iter().enumerate() {
        for (c, &val) in row.iter().enumerate() {
            if val == 9 {
                println!("found 9 at row {r}, column {c}");
                break 'rows;
            }
        }
        println!("row {r} has no 9");
    }
}

We search a two-dimensional array for the value 9.

'rows: for (r, row) in grid.iter().enumerate() {

The outer loop is labelled 'rows.

break 'rows;

When the value is found, we end both loops. A plain break would end only the inner loop, and the outer loop would continue with the third row.

$ cargo run -q
row 0 has no 9
found 9 at row 1, column 1

The match expression

The match expression compares a value against a series of patterns and executes the code of the first matching arm. The patterns must cover all possible values. It is covered in detail in the Rust match expression article.

main.rs
fn main() {
    let grades = ["A", "B", "C", "D", "E", "FX", "Z"];

    for grade in grades {
        let res = match grade {
            "A" | "B" | "C" | "D" | "E" => "passed",
            "FX" => "failed",
            _ => "unknown grade",
        };

        println!("{grade}: {res}");
    }
}

We go over an array of grades and print whether each grade is passing.

"A" | "B" | "C" | "D" | "E" => "passed",

With the | operator, one arm matches several values.

_ => "unknown grade",

The _ wildcard matches any value. Since a string can hold any text, the compiler requires this arm to make the match exhaustive.

$ cargo run -q
A: passed
B: passed
C: passed
D: passed
E: passed
FX: failed
Z: unknown grade

The if let expression

The if let expression runs a block only when a value matches a pattern. It is a shorter form of a match with a single interesting arm.

main.rs
fn main() {
    let words = ["sky", "forest", "cup"];

    if let Some(pos) = words.iter().position(|&w| w == "forest") {
        println!("forest is at index {pos}");
    }

    if let Some(pos) = words.iter().position(|&w| w == "pen") {
        println!("pen is at index {pos}");
    } else {
        println!("pen not found");
    }
}

The position method returns an Option with the index of the searched element.

if let Some(pos) = words.iter().position(|&w| w == "forest") {
    println!("forest is at index {pos}");
}

If the result is Some, the index is bound to pos and the block is executed. If it is None, nothing happens.

} else {
    println!("pen not found");
}

An if let can have an else branch for the values that do not match the pattern.

$ cargo run -q
forest is at index 1
pen not found

The while let loop

The while let loop runs as long as a value matches a pattern.

main.rs
fn main() {
    let mut stack = vec![1, 2, 3, 4];

    while let Some(top) = stack.pop() {
        print!("{top} ");
    }

    println!();
    println!("stack is empty: {}", stack.is_empty());
}

We use a vector as a stack and take its elements from the top.

while let Some(top) = stack.pop() {
    print!("{top} ");
}

The pop method removes the last element and returns it in Some. When the vector is empty, it returns None and the loop ends.

$ cargo run -q
4 3 2 1 
stack is empty: true

The let else statement

The let else statement binds a variable if the value matches the pattern. If it does not, the else block is executed; this block must leave the current scope, for instance with return, break, or a panic.

main.rs
fn parse_age(input: &str) -> Option<u32> {
    let Ok(age) = input.trim().parse::<u32>() else {
        println!("'{input}' is not a valid age");
        return None;
    };

    Some(age)
}

fn main() {
    println!("{:?}", parse_age("34"));
    println!("{:?}", parse_age("old"));
}

The parse_age function converts a string into an age.

let Ok(age) = input.trim().parse::<u32>() else {
    println!("'{input}' is not a valid age");
    return None;
};

If parsing succeeds, the value is bound to age, which is available in the rest of the function. Otherwise, we print a message and return early. This keeps the main path of the function free of nested blocks.

$ cargo run -q
Some(34)
'old' is not a valid age
None

Source

The Rust Programming Language: Control Flow

The Rust Reference: Loops and other breakable expressions

In this article we have worked with control flow structures in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.