Free Handbook · Every example compiled & verified

Flow Control & Functions

if as an expression, loop with a break value, while, for over ranges, labels, match basics, functions, and the trailing-semicolon trap.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 02 · what you'll be able to do

  • Use if, else if and else as expressions that produce a value, and explain why both branches must have the same type
  • Choose between loop, while and for, return a value from loop with break, and break out of nested loops with labels
  • Iterate over ranges, arrays and vectors with for, including rev(), step_by() and enumerate()
  • Write a basic match on numbers with ranges, | and the _ catch-all
  • Write functions with typed parameters and return values, and tell an expression from a statement well enough to never fall into the trailing-semicolon trap
01

if, else if, else — and if as an expression

An if in Rust looks like the one you know, with two differences. The condition needs no parentheses, and it must be a real bool — Rust never treats 0, an empty string or a missing value as false. The braces around each branch are always required, even for one line.

rustmain.rs
fn main() {
    let temperature = 31;

    if temperature > 30 {
        println!("hot");
    } else if temperature > 15 {
        println!("mild");
    } else {
        println!("cold");
    }

    let items = 3;
    if items != 0 {                 // say what you mean: != 0
        println!("cart has {items} items");
    }
}
Outputcompiled & run with real Rust
hot
cart has 3 items
Error you will hit

E0308: an integer is not a bool

rust
fn main() {
    let items = 3;
    if items {
        println!("cart is not empty");
    }
}
error[E0308]: mismatched types
 --> main.rs:3:8
  |
3 |     if items {
  |        ^^^^^ expected `bool`, found integer
Why the compiler said that

C, JavaScript and Python quietly treat a non-zero number as true. Rust has no "truthy" values: the condition of an if must have type bool, so an integer is a type mismatch.

The fix

Write the comparison you actually mean: items != 0 or items > 0.

rust
fn main() {
    let items = 3;
    if items > 0 {
        println!("cart is not empty");
    }
}

if produces a value

In Rust almost everything is an expression — something that evaluates to a value. An if/else is one of them: the value of the whole thing is the value of the branch that ran. That replaces the ternary operator (cond ? a : b), which Rust does not have.

rustmain.rs
fn main() {
    let score = 72;
    let grade = if score >= 90 {
        'A'
    } else if score >= 70 {
        'B'
    } else {
        'C'
    };                                   // the let still ends with ;
    println!("grade {grade}");

    let stock = 0;
    let label = if stock > 0 { "in stock" } else { "sold out" };
    println!("{label}");
}
Outputcompiled & run with real Rust
grade B
sold out
Your turn

Delete the final else branch from grade and compile. The compiler says an if without else evaluates to () — why can that never be a char?

Error you will hit

E0308: if and else have incompatible types

rust
fn main() {
    let in_stock = true;
    let label = if in_stock { 5 } else { "sold out" };
    println!("{label}");
}
error[E0308]: `if` and `else` have incompatible types
 --> main.rs:3:42
  |
3 |     let label = if in_stock { 5 } else { "sold out" };
  |                               -          ^^^^^^^^^^ expected integer, found `&str`
  |                               |
  |                               expected because of this
Why the compiler said that

A variable has exactly one type, fixed at compile time. If one branch gives an integer and the other a string slice, the compiler cannot say what type label is, so every branch of an if used as a value must produce the same type.

The fix

Make both branches return the same type. Here both should be text.

rust
fn main() {
    let in_stock = true;
    let label = if in_stock { "5 left" } else { "sold out" };
    println!("{label}");
}
02

loop, while and loop labels

Rust has three loops. loop runs forever until you break. while runs as long as a condition is true. for walks over a sequence (next lesson). continue skips to the next round in all three.

Because loop is an expression, break can carry a value out of it: break value;. That is the idiomatic way to "retry until it works, then keep the result".

rustmain.rs
fn main() {
    // loop + break with a value: find the first power of 3 above 100
    let mut n = 1;
    let first_big = loop {
        n *= 3;
        if n > 100 {
            break n;             // the loop evaluates to n
        }
    };
    println!("first power of 3 above 100: {first_big}");

    // while: count down
    let mut t = 3;
    while t > 0 {
        println!("{t}...");
        t -= 1;
    }
    println!("liftoff");
}
Outputcompiled & run with real Rust
first power of 3 above 100: 243
3...
2...
1...
liftoff
Visualizeloop with a break valueStep 1 / 7
fn main() {
let mut n = 1;
let first_big = loop {
n *= 3;
if n > 100 {
break n;
}
};
println!("{first_big}");
}
Line 2

n starts at 1.

Variables now
n1
All 7 steps as a table
StepLineWhat happenedVariables now
12n starts at 1.n = 1
24Round 1: n becomes 3. 3 is not above 100, so the loop goes round again.n = 3
34Round 2: 9. Still not above 100.n = 9
44Round 3: 27. Round 4: 81. Neither is above 100.n = 81
54Round 5: n becomes 243.n = 243
66243 > 100, so break n ends the loop and hands 243 to the let.n = 243 first_big = 243
79Print the value the loop produced.
Error you will hit

E0571: break with a value from a while loop

rust
fn main() {
    let mut n = 1;
    let found = while n < 100 {
        n *= 3;
        if n % 2 == 0 {
            break n;
        }
    };
    println!("{found}");
}
error[E0571]: `break` with value from a `while` loop
 --> main.rs:6:13
  |
3 |     let found = while n < 100 {
  |                 ------------- you can't `break` with a value in a `while` loop
...
6 |             break n;
  |             ^^^^^^^ can only break with a value inside `loop` or breakable block
  |
help: use `break` on its own without a value inside this `while` loop
  |
6 -             break n;
6 +             break;
  |
Why the compiler said that

A while loop can also end because its condition turned false — and then there would be no value to hand back. Only loop can end solely through break, so only loop can produce a value.

The fix

Use loop and move the condition inside it, or keep the while and store the result in a variable declared before the loop (usually an Option, Module 04).

rust
fn main() {
    let mut n = 1;
    let found = loop {
        n *= 3;
        if n >= 100 {
            break 0;        // gave up
        }
        if n % 2 == 0 {
            break n;
        }
    };
    println!("{found}");
}

Loop labels: breaking out of nested loops

A plain break or continue affects the innermost loop. To target an outer one, give it a label — a name starting with an apostrophe, like 'outer: — and write break 'outer;.

rustmain.rs
fn main() {
    let grid = [[1, 2, 3], [4, 42, 6], [7, 8, 9]];
    let mut found = (0, 0);

    'rows: for (r, row) in grid.iter().enumerate() {
        for (c, &value) in row.iter().enumerate() {
            if value == 42 {
                found = (r, c);
                break 'rows;       // leave BOTH loops
            }
        }
    }
    println!("42 is at row {}, column {}", found.0, found.1);

    'outer: for i in 1..=3 {
        for j in 1..=3 {
            if j == 2 {
                continue 'outer;   // skip the rest of this i
            }
            println!("i={i} j={j}");
        }
    }
}
Outputcompiled & run with real Rust
42 is at row 1, column 1
i=1 j=1
i=2 j=1
i=3 j=1
03

for over ranges and iterators

for is the loop you will write most. It walks over anything that can produce an iterator: a range, an array, a vector, the characters of a string. There is no C-style for (i = 0; i < n; i++) — you write a range instead, which also rules out off-by-one mistakes in the condition.

RangeYieldsNote
0..50 1 2 3 4End is exclusive
1..=51 2 3 4 5End is inclusive
(0..5).rev()4 3 2 1 0Backwards
(0..10).step_by(3)0 3 6 9Every third
rustmain.rs
fn main() {
    for i in 0..3 {
        print!("{i} ");
    }
    println!();

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

    for i in (0..10).step_by(3) {
        print!("{i} ");
    }
    println!();

    let fruits = ["apple", "banana", "cherry"];
    for fruit in fruits {
        print!("{fruit} ");
    }
    println!();

    for (index, fruit) in fruits.iter().enumerate() {
        println!("{index}: {fruit}");
    }

    for ch in "héllo".chars() {
        print!("[{ch}]");
    }
    println!();
}
Outputcompiled & run with real Rust
0 1 2 
3 2 1 
0 3 6 9 
apple banana cherry 
0: apple
1: banana
2: cherry
[h][é][l][l][o]
Your turn

Print the multiplication table for 7 from 7 x 1 to 7 x 10 with a single for over 1..=10.

for over a Vec: v, &v or &mut v
for x in v consumes the vector — you cannot use v afterwards (that is ownership, Module 03). for x in &v borrows it and gives you &T; for x in &mut v lets you change each element through *x. Arrays of numbers are Copy, which is why for fruit in fruits above left fruits usable.
rustmain.rs
fn main() {
    let mut prices = vec![100, 250, 80];

    for p in &mut prices {
        *p = *p * 9 / 10;          // 10% off, in place
    }

    let mut total = 0;
    for p in &prices {
        total += p;
    }
    println!("{:?} total {total}", prices);
}
Outputcompiled & run with real Rust
[90, 225, 72] total 387
VisualizeSumming with a for loopStep 1 / 6
fn main() {
let prices = [90, 225, 72];
let mut total = 0;
for p in prices {
total += p;
}
println!("total {total}");
}
Line 2

An array of three prices.

Variables now
prices[90, 225, 72]
All 6 steps as a table
StepLineWhat happenedVariables now
12An array of three prices.prices = [90, 225, 72]
23The running total starts at 0.total = 0
35First element: p is 90.p = 90 total = 90
45Second element: p is 225.p = 225 total = 315
55Third element: p is 72.p = 72 total = 387
67The iterator is exhausted, the loop ends and p goes out of scope.
04

match basics

match compares a value against a list of patterns and runs the first arm that fits. Think of it as a switch that cannot fall through, that is an expression, and that the compiler checks is exhaustive — every possible value must be handled. The _ pattern matches anything and is the usual "everything else" arm.

rustmain.rs
fn main() {
    for code in [200, 301, 404, 418, 503] {
        let meaning = match code {
            200 => "ok",
            301 | 302 => "redirect",          // either value
            400..=499 => "client error",       // inclusive range
            500..=599 => "server error",
            _ => "something else",             // catch-all
        };
        println!("{code}: {meaning}");
    }

    let n = 7;
    match n % 2 {
        0 => println!("{n} is even"),
        _ => println!("{n} is odd"),
    }
}
Outputcompiled & run with real Rust
200: ok
301: redirect
404: client error
418: client error
503: server error
7 is odd

The second match is used as a statement: each arm just prints, so the whole match evaluates to ().

Arms are tried top to bottom, so put specific patterns before general ones: 418 placed after 400..=499 would never be reached (the compiler warns you that the arm is unreachable). Remove the _ arm from a match on an integer and the program no longer compiles, because some numbers would have no arm. Module 04 covers the full pattern language: destructuring structs and enums, guards, @ bindings, if let and let else.

05

Functions, parameters and return values

Functions are declared with fn, named in snake_case, and can be defined before or after the code that calls them. Every parameter must have a type — Rust infers types inside a function body, never in its signature. The return type comes after ->; leave it out and the function returns (), the unit type.

rustmain.rs
fn main() {
    greet("Ada", 3);
    let area = rectangle_area(4.0, 2.5);
    println!("area = {area}");
    let (lo, hi) = min_max(8, 3);
    println!("min {lo}, max {hi}");
}

fn greet(name: &str, times: u32) {
    for _ in 0..times {
        print!("hi {name}! ");
    }
    println!();
}

fn rectangle_area(width: f64, height: f64) -> f64 {
    width * height                    // no semicolon: this is the return value
}

fn min_max(a: i32, b: i32) -> (i32, i32) {
    if a < b { (a, b) } else { (b, a) }   // return two values as a tuple
}
Outputcompiled & run with real Rust
hi Ada! hi Ada! hi Ada! 
area = 10
min 3, max 8
Your turn

Write fn is_even(n: i32) -> bool and call it from main for 4 and 7.

Expressions vs statements

This is the one idea that makes Rust functions click. An expression evaluates to a value: 5, a + b, a function call, an if, a match, a block { ... }. A statement performs an action and produces no value: let x = 5;, or any expression followed by a semicolon. Adding a ; turns an expression into a statement and throws its value away. The last expression of a block, with no semicolon, is the value of the block — and the block that forms a function body is the function's return value.

rustmain.rs
fn main() {
    let y = {
        let x = 3;
        x * x + 1          // tail expression: the block's value
    };
    println!("y = {y}");

    let nothing = {
        let x = 3;
        x * x + 1;         // semicolon: value thrown away, block is ()
    };
    println!("nothing = {:?}", nothing);
}
Outputcompiled & run with real Rust
y = 10
nothing = ()
Error you will hit

E0308: the trailing-semicolon trap

rust
fn add(a: i32, b: i32) -> i32 {
    a + b;
}

fn main() {
    println!("{}", add(2, 3));
}
error[E0308]: mismatched types
 --> main.rs:1:27
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |    ---                    ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     a + b;
  |          - help: remove this semicolon to return this value
Why the compiler said that

The semicolon after a + b made it a statement. The body now ends with no tail expression, so it evaluates to () — but the signature promised an i32. Every Rust beginner hits this in the first week; the compiler even points at the exact semicolon.

The fix

Delete the semicolon so a + b is the tail expression. (Writing return a + b; also works, but is not idiomatic for the last line.)

rust
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    println!("{}", add(2, 3));
}
Reading code in real jobs
In Rust codebases you will see functions whose whole body is a single match or if with no return anywhere. That is not clever code; it is the normal style. When you review, scan for the last line without a semicolon to find what a function returns.
06

Early return, recursion and shadowing

Use return value; to leave a function before its end. The common pattern is the guard clause: deal with the edge cases first and return, so the main logic is not buried inside nested ifs. The final result is then an ordinary tail expression.

rustmain.rs
fn shipping_cost(weight_kg: f64, express: bool) -> f64 {
    if weight_kg <= 0.0 {
        return 0.0;                  // guard: nothing to ship
    }
    if weight_kg > 30.0 {
        return 99.0;                 // guard: flat freight price
    }
    let base = 5.0 + weight_kg * 1.5;
    if express { base * 2.0 } else { base }
}

fn main() {
    println!("{}", shipping_cost(0.0, false));
    println!("{}", shipping_cost(2.0, false));
    println!("{}", shipping_cost(2.0, true));
    println!("{}", shipping_cost(45.0, true));
}
Outputcompiled & run with real Rust
0
8
16
99

Recursion

A function may call itself. Every recursive function needs a base case that stops the calls; without one the program overflows the stack and aborts. Rust does not guarantee tail-call optimisation, so for deep inputs a loop is safer.

rustmain.rs
fn factorial(n: u64) -> u64 {
    if n == 0 {
        1                              // base case
    } else {
        n * factorial(n - 1)           // recursive case
    }
}

fn main() {
    for n in [0, 1, 5, 10, 20] {
        println!("{n}! = {}", factorial(n));
    }
}
Outputcompiled & run with real Rust
0! = 1
1! = 1
5! = 120
10! = 3628800
20! = 2432902008176640000
Your turn

21! does not fit in a u64. Call factorial(21) and read the panic, then rewrite the multiply with checked_mul (Module 01).

Visualizefactorial(3), call by callStep 1 / 7
fn factorial(n: u64) -> u64 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}
fn main() {
println!("{}", factorial(3));
}
Line 10

main calls factorial(3).

Variables now

nothing yet

All 7 steps as a table
StepLineWhat happenedVariables now
110main calls factorial(3).
25n is 3, not 0, so it needs factorial(2) before it can multiply.n = 3
35A new call with its own n = 2 needs factorial(1).n = 2
45n = 1 needs factorial(0).n = 1
53n = 0 hits the base case and returns 1.n = 0
65Unwinding: 1 * 1 = 1, then 2 * 1 = 2, then 3 * 2 = 6.n = 3
710factorial(3) returned 6.

Shadowing, recapped

You met shadowing in Module 01. Inside functions it is how you refine a parameter in steps without inventing new names — and a shadow declared inside a block or loop body disappears when that block ends.

rustmain.rs
fn normalise(name: &str) -> String {
    let name = name.trim();              // &str, whitespace removed
    let name = name.to_lowercase();      // now a String
    let name = name.replace(' ', "-");   // still a String
    name
}

fn main() {
    let id = normalise("  Rust Handbook ");
    println!("[{id}]");
}
Outputcompiled & run with real Rust
[rust-handbook]
Expression
Code that evaluates to a value: literals, operators, calls, blocks, if, match, loop.
Statement
Code that performs an action and has no value: let declarations and expressions ended with ;.
Tail expression
The last expression in a block with no semicolon. It is the value of the block, and of a function body.
Range
a..b (end excluded) or a..=b (end included). Iterable with for.
Loop label
A name like 'outer on a loop, so break 'outer or continue 'outer can target it from inside a nested loop.
Guard clause
An early return that handles an edge case at the top of a function.
Exhaustive match
A match whose arms cover every possible value. The compiler enforces it.
Quick check

fn double(x: i32) -> i32 { x * 2; } — what happens when you compile this?

Quick check

Which loop can return a value with break value?

Frequently asked questions

Does Rust have a ternary operator?
No. if is an expression, so let x = if cond { a } else { b }; does the same job. Both branches must have the same type.
What is the difference between loop, while and for in Rust?
loop repeats until a break and can return a value with break value. while repeats while a condition is true. for walks over an iterator such as a range (0..10), an array or a vector, and is the one you will use most.
Why does removing a semicolon change what a Rust function returns?
The last expression in a block without a semicolon is the value of the block. A function body is a block, so that expression is the return value. A semicolon turns it into a statement whose value is thrown away, and the body then evaluates to ().

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.