Control Flow: if, loop, while & for

Rust · Lesson 7 of 31 6 min read Updated

Programs need to make decisions and repeat work: give a discount only to members, keep asking until the input is valid, print a line for every student in a list. These are jobs for control flow.

Rust has one way to choose (if, plus match, which gets its own lesson in Enums & match) and three kinds of loop: loop, while and for. Like most things in Rust, if and loop are expressions, so they can produce values.

if, else if and else

if runs a block when a condition is true. Add else if for more cases and else for everything else. The condition needs no brackets, but the curly brackets around each block are required.

Rust
fn main() {
    let temperature = 38;

    if temperature >= 40 {
        println!("Heatwave alert: stay indoors");
    } else if temperature >= 30 {
        println!("Hot day: carry water");
    } else {
        println!("Pleasant weather");
    }

    let label = if temperature % 2 == 0 { "even" } else { "odd" };
    println!("{temperature} is {label}");
}
Output
Hot day: carry water
38 is even

Rust checks the conditions from top to bottom and runs only the first block whose condition is true. The second if is used as an expression: its value goes straight into label. This replaces the ?: ternary operator of other languages. When if is used for a value, every branch must produce the same type, and an else is required.

The condition must be a real bool. Unlike C or JavaScript, Rust does not treat numbers as true or false, so if tickets { ... } with tickets being an integer is a compile error ("expected bool, found integer"). Write what you mean: if tickets > 0.

loop: repeat until you break

loop repeats forever until the code inside it runs break. You can hand a value back with break value, which makes the whole loop an expression:

Rust
fn main() {
    let mut balance = 500;
    let mut recharges = 0;

    let count = loop {
        balance -= 120;
        recharges += 1;
        if balance < 120 {
            break recharges;
        }
    };

    println!("Rs. 120 recharges possible: {count}");
    println!("Balance left: Rs. {balance}");
}
Output
Rs. 120 recharges possible: 4
Balance left: Rs. 20

Trace it by hand: the balance goes 500, 380, 260, 140, 20. After the fourth recharge it is below 120, so the loop breaks with the value 4. loop is the right choice when the exit condition is found somewhere in the middle of the work, such as retrying a network request.

while: repeat while a condition is true

while checks its condition before each pass and stops as soon as it is false:

Rust
fn main() {
    let mut countdown = 5;
    while countdown > 0 {
        print!("{countdown} ");
        countdown -= 1;
    }
    println!("Lift-off!");
}
Output
5 4 3 2 1 Lift-off!

print! is like println! but does not add a new line, so the numbers stay on one line.

for: walk through ranges and collections

for is the loop you will write most often. It goes through every item of anything that can be iterated: a range of numbers, an array, a vector and many more.

Rust
fn main() {
    for n in 1..=3 {
        println!("7 x {n} = {}", 7 * n);
    }

    let cities = ["Rohtak", "Jaipur", "Pune"];
    for city in cities {
        println!("Delivering to {city}");
    }

    for (i, city) in cities.iter().enumerate() {
        println!("{}. {city}", i + 1);
    }

    for n in (1..=4).rev() {
        print!("{n} ");
    }
    println!();
}
Output
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
Delivering to Rohtak
Delivering to Jaipur
Delivering to Pune
1. Rohtak
2. Jaipur
3. Pune
4 3 2 1 
  • 1..=3 is an inclusive range: 1, 2 and 3. 1..3 is exclusive of the end: 1 and 2.
  • for city in cities takes each item of the array in turn. Here the array can still be used afterwards, because its items are simple copyable values.
  • .enumerate() pairs each item with its index (starting at 0), which is why we print i + 1.
  • .rev() reverses the range, and println!() with nothing inside just ends the line.

A for loop over a collection cannot go past the end, so it is safer than counting indexes by hand. Prefer for item in list over for i in 0..list.len().

continue, break and loop labels

continue skips the rest of the current pass and moves to the next one. break leaves the loop. With nested loops, break only leaves the innermost loop, unless you give the outer loop a label (a name starting with an apostrophe) and break to that label:

Rust
fn main() {
    // 1 = seat taken, 0 = seat free
    let hall = [[1, 1, 1], [1, 0, 1], [0, 0, 1]];
    let mut seat = (0, 0);

    'rows: for (r, row) in hall.into_iter().enumerate() {
        for (c, taken) in row.into_iter().enumerate() {
            if taken == 1 {
                continue;
            }
            seat = (r + 1, c + 1);
            break 'rows;
        }
    }

    println!("First free seat: row {}, seat {}", seat.0, seat.1);
}
Output
First free seat: row 2, seat 2

The first row is full, so every pass hits continue. In the second row, the second seat is free; the code records it and break 'rows leaves both loops at once. Without the label, the outer loop would carry on and overwrite the answer with a seat from row 3.

Common mistakes

  • Using a number as a condition. Write if count > 0, not if count.
  • Off-by-one ranges. 1..10 stops at 9. Use 1..=10 to include 10.
  • Missing else when if gives a value. let x = if ok { 1 }; does not compile, because there would be no value when ok is false.
  • Forgetting to change the while variable. If countdown -= 1; is missing, the loop never ends.
  • Indexing in loops. for i in 0..v.len() works but is easy to get wrong; iterate over the items directly.

Key points

  • if/else if/else chooses a branch; conditions must be bool.
  • if is an expression, so let x = if c { a } else { b }; replaces the ternary operator.
  • loop repeats until break, and break value makes the loop produce a value.
  • while repeats while a condition holds; for walks through ranges (a..b, a..=b) and collections.
  • continue skips to the next pass, and labelled breaks such as break 'rows leave nested loops.

Exercise

Print the FizzBuzz sequence for 1 to 15 with a twist for a cricket club: print "Six" for multiples of 6, "Four" for multiples of 4, "Boundary" for multiples of both, and the number otherwise. Then use a loop with break value to find the first number above 50 that is a multiple of both 4 and 6.

Show solution
Rust
fn main() {
    for n in 1..=15 {
        if n % 4 == 0 && n % 6 == 0 {
            println!("Boundary");
        } else if n % 6 == 0 {
            println!("Six");
        } else if n % 4 == 0 {
            println!("Four");
        } else {
            println!("{n}");
        }
    }

    let mut n = 51;
    let first = loop {
        if n % 4 == 0 && n % 6 == 0 {
            break n;
        }
        n += 1;
    };
    println!("First Boundary above 50: {first}");
}
Output
1
2
3
Four
5
Six
7
Four
9
10
11
Boundary
13
14
15
First Boundary above 50: 60

The check for both multiples must come first; otherwise 12 would match the "Six" branch and never reach "Boundary". && means "and".

Quick Quiz

1. What does for n in 1..4 loop over?

2. Which line is valid Rust?

3. What value does x get? let mut i = 0; let x = loop { i += 3; if i > 7 { break i * 2; } };

4. How do you leave an outer loop from inside an inner loop?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call