Rust · Beginner

Control Flow in Rust: if, loop, while and for

9 min readUpdated September 24, 2026Every example verified

In short: Rust branches with if, whose condition must be a bool and which can produce a value, and repeats with three loops: loop runs until break, while runs while a condition holds, and for walks through a range or a collection.

Branching and repeating

Control flow in Rust looks familiar and then differs in two ways that matter. First, the condition of an if or while must be a bool. An integer, a string or an Option is not accepted, so if count { does not compile; you write if count > 0 {. There are no parentheses around the condition, and the braces around each branch are compulsory. Second, if is an expression: it produces a value, so let fare = if age < 4 { 0 } else { 7 }; is ordinary Rust, and there is no separate ternary operator. When if is used for its value, every branch must produce the same type and an else is required, because the compiler has to know what fare is when the condition is false.

loop repeats its body until a break. Because it is guaranteed to run until you stop it, break may carry a value, and the whole loop expression evaluates to that value: let found = loop { ...; break index; };. This is the shape for retrying, polling or searching when the exit condition sits in the middle of the body. while condition { } checks its bool before every pass and stops when it is false. Rust has no do-while; write a loop with a break at the end instead.

for is the loop you will write most. It walks through anything iterable: a range, an array, a Vec, the lines of standard input. 1..=4 counts 1 to 4 inclusive; 0..4 stops before 4 and matches how indexes work, so 0..items.len() visits every valid index. (1..=4).rev() counts down. The loop variable is a fresh binding each pass, so assigning to it does not affect the iteration. There is no C-style for (i = 0; i < n; i++); a range says the same thing without an off-by-one to get wrong. Iterating for t in temps over an array visits each element by value; for t in &temps borrows them instead, which matters once the ownership rules come into play.

Inside any loop, continue jumps to the next pass and break leaves. When loops are nested, a label on the outer loop, written 'outer: for ..., lets break 'outer or continue 'outer act on it from inside the inner loop, which removes the flag variables other languages need.

match, Rust's multi-way branch, deserves its own page: see pattern matching. For a two-way or three-way decision, if and else if are the right tool.

Syntax

 Rust · syntax
if hours > 8 {
    println!("overtime");
} else if hours == 8 {
    println!("full day");
} else {
    println!("part day");
}
let rate = if hours > 8 { 15 } else { 12 };   // if as an expression

let mut n = 0;
let result = loop {          // repeat until break; break can carry a value
    n += 1;
    if n * n > 50 { break n; }
};

while n > 0 {                // repeat while the condition is true
    n -= 1;
}

for i in 0..4 { }            // 0, 1, 2, 3
for i in 1..=4 { }           // 1, 2, 3, 4
for i in (1..=4).rev() { }   // 4, 3, 2, 1
for t in [18, 21, 19] { }    // each element of an array

'outer: for a in 1..4 {
    for b in 1..4 {
        if a * b == 6 { break 'outer; }
    }
}

Conditions are bare bool expressions with no parentheses; braces are never optional. .. excludes the end of a range and ..= includes it.

if as an expression, applied to each line of input

Four ages arrive on standard input; the if chain turns each into a fare, and the whole chain is assigned to fare.

 Rust
use std::io::BufRead;

fn main() {
    for line in std::io::stdin().lock().lines() {
        let age: u32 = line.unwrap().trim().parse().unwrap();
        let fare = if age < 4 {
            0
        } else if age < 16 {
            3
        } else if age >= 65 {
            4
        } else {
            7
        };
        println!("Age {}: fare {}", age, fare);
    }
}

Input given to the program: 2114070

Output

Age 2: fare 0
Age 11: fare 3
Age 40: fare 7
Age 70: fare 4

for line in std::io::stdin().lock().lines() visits every input line; each is parsed into a u32. The if chain is one expression whose value becomes fare, so there is no need to declare fare as mutable and assign in each branch. All four branches produce an integer of the same type, and the final else guarantees a value for any age. The chain is tested in order: an age of 70 is not under 16, so it reaches the age >= 65 branch.

loop with a break value, then while

The loop cannot know in advance how many passes it needs; the while runs only while more than 20 litres remain.

 Rust
fn main() {
    let mut litres = 0;
    let mut minutes = 0;
    let filled_after = loop {
        litres += 7;
        minutes += 1;
        if litres >= 50 {
            break minutes;
        }
    };
    println!("Tank full after {} minutes ({} litres)", filled_after, litres);

    let mut remaining = litres;
    while remaining > 20 {
        remaining -= 15;
        println!("Watering... {} litres left", remaining);
    }
    println!("Stop: {} litres is below the limit", remaining);
}

Output

Tank full after 8 minutes (56 litres)
Watering... 41 litres left
Watering... 26 litres left
Watering... 11 litres left
Stop: 11 litres is below the limit

The loop fills the tank in steps of 7 litres, so the exit test sits in the middle of the body and break minutes hands the pass count out as the value of the whole loop. The while loop checks its condition before each pass: when remaining reaches 11 the body does not run again and the final message prints. Both loops modify mut variables declared outside them.

for over ranges, an array and a labelled break

Four loops: an inclusive range, a reversed range, an index range with continue, and nested loops left with one labelled break.

 Rust
fn main() {
    for stop in 1..=3 {
        println!("Stop {}", stop);
    }
    for n in (1..=3).rev() {
        print!("{} ", n);
    }
    println!("depart");

    let loads = [40, 55, 0, 62];
    for i in 0..loads.len() {
        if loads[i] == 0 {
            continue;
        }
        println!("Bay {} carries {} kg", i + 1, loads[i]);
    }

    'shelves: for shelf in 1..=3 {
        for slot in 1..=4 {
            if shelf * slot > 6 {
                println!("First overload at shelf {} slot {}", shelf, slot);
                break 'shelves;
            }
        }
    }
}

Output

Stop 1
Stop 2
Stop 3
3 2 1 depart
Bay 1 carries 40 kg
Bay 2 carries 55 kg
Bay 4 carries 62 kg
First overload at shelf 2 slot 4

The first loop counts 1 to 3 inclusive with ..=; the second reverses a range to count down, using print! to keep the numbers on one line. The third loop uses 0..loads.len() to visit every valid index and continue to skip the empty bay. The nested loops search shelves and slots for the first product that exceeds 6; break 'shelves leaves both loops at once, so only the first hit is printed.

Common mistakes

  • Using a number or a string as a condition

    Why it goes wrong: if count { } is a type mismatch (E0308: expected bool, found integer). Rust has no truthiness rule, so nothing is silently converted to bool.

    Fix: Write the comparison you mean: if count > 0, if !name.is_empty().

  • Using if as a value without an else

    Why it goes wrong: let x = if c { 1 }; gives error E0317, if may be missing an else clause. Without else the expression has no value when the condition is false, so it cannot be assigned.

    Fix: Add an else branch that produces the same type, or use a plain if statement and assign inside it.

     Rust · fix
    let discount = if member { 10 } else { 0 };
  • Off-by-one with .. and ..=

    Why it goes wrong: 1..5 yields 1 to 4; the end is excluded so that 0..len fits indexes exactly. Writing 1..5 when you meant to include 5 silently drops the last pass.

    Fix: Use 1..=5 when the end value belongs in the loop, and 0..items.len() for indexes.

  • A while loop whose condition never changes

    Why it goes wrong: while remaining > 0 { println!(...) } runs forever if the body never updates remaining. The compiler cannot detect this; the program simply never finishes.

    Fix: Make sure the body changes something the condition depends on, or use for over a range when the number of passes is known.

Choosing a loop

LoopUse whenCan break with a value?
loopthe exit condition is discovered inside the body, or the body must run at least onceyes
while condyou repeat while some state holds and the number of passes is unknownno
for x in iterableyou visit each item of a range or collectionno

Where you use this

A ticket machine at a harbour reads passenger ages one per line and prints each fare, which is exactly the shape of the first example: a for over the input lines and an if expression inside. The same tool needs a retry loop when the card reader is busy: loop, try to charge, break with the receipt number on success, and give up after a fixed number of attempts with a labelled break from the inner polling loop. Every exercise on this site that says a number of lines follow on standard input is a for over lines(), and every one that asks you to count down or up is a range.

 Rust · in practice
for line in std::io::stdin().lock().lines() {
    let age: u32 = line.unwrap().trim().parse().unwrap();
    let fare = if age < 16 { 3 } else { 7 };
    println!("{}", fare);
}

Key points

  • An if condition must be a bool; there are no parentheses and braces are required.
  • if is an expression: with an else, it produces a value, and all branches must have the same type.
  • loop repeats until break, and break value makes the loop evaluate to that value.
  • while tests a bool before each pass; Rust has no do-while.
  • for x in a..b excludes b, a..=b includes it, and .rev() counts down.
  • continue skips to the next pass; a label such as 'outer: lets break or continue target an enclosing loop.

Try it yourself

The program reads a number of laps and prints it. Add a countdown that prints each lap number from laps down to 1 on its own line, using a reversed inclusive range, and then prints Go.

Your program
use std::io::BufRead;

fn main() {
    let mut lines = std::io::stdin().lock().lines();
    let laps: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
    println!("Laps: {}", laps);
}
Input the program receives: 3
Expected output: Laps: 3 3 2 1 Go

Practise this

Open the Rust playground

Frequently asked questions

Does Rust have a ternary operator?

No, and it does not need one. if is an expression, so let fee = if urgent { 12 } else { 5 }; does what urgent ? 12 : 5 does in C-family languages, with the same requirement that both branches have one type. For longer chains, else if or a match reads better than nested conditionals.

How do I loop over the indexes and values of an array in Rust?

For indexes alone, iterate the range 0..arr.len() and index inside the loop. For both at once, use for (i, value) in arr.iter().enumerate(), which yields pairs of an index and a reference to each element; the collections and iterators lesson covers enumerate and its relatives.

Why does Rust not have a C-style for loop?

A range expresses the same iteration more safely. for i in 0..n cannot forget to increment, cannot compare with the wrong bound, and makes the loop variable a fresh binding on every pass. When you need a different step, (0..n).step_by(2) or (0..n).rev() cover the common cases, and while handles anything irregular.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.