Control Flow & Functions
So far your programs run straight down, top to bottom. Real programs branch ("if it's hot, say so"),
repeat ("for each score, print it"), and split work into named, reusable pieces (functions). Along the way
you'll meet two ideas that make Rust feel different from most languages: almost everything is an
expression that produces a value, and match forces you to handle every possible case. That second
one is the star of the phase - the feature Rust programmers miss most when they go back to other languages.
if / else - and it's an expression
You know if/else. The twist in Rust: an if doesn't just do something, it produces a value, so you
can assign its result directly to a variable.
$ cargo run
hot
What just happened: if temp > 25 { "hot" } else { "mild" } evaluated to one of the two strings and
let label = caught it. There's no ternary ? : in Rust because plain if already does that job. (Both
branches must produce the same type.)
📝 Terminology. An expression produces a value (2 + 2, if ... { } else { }). A statement
does something but produces no usable value (let x = 5;). Rust leans hard on expressions - keep this
distinction in mind; it explains how functions return, below.
The loops: for, while, loop
Rust has three ways to repeat, each for a different shape of problem.
for ... in - walking over a collection or a range. This is the one you'll use most:
$ cargo run
10
20
30
i = 0
i = 1
i = 2
What just happened: The first loop walked the array. The second walked a range, 0..3, meaning "0 up
to but not including 3" - so 0, 1, 2. (To include the end, use 0..=3 for 0, 1, 2, 3.)
while - repeats as long as a condition holds:
$ cargo run
count 0
count 1
count 2
What just happened: The loop ran while count < 3, printing and incrementing each pass, then stopped
once count reached 3. (Forgetting mut on count bites people here, as warned in
Phase 2.)
loop - repeats forever until you break out. Useful when the exit condition is in the middle, not
the top:
$ cargo run
n = 1
n = 2
n = 3
What just happened: loop runs unconditionally; break is the only way out (here, once n > 3). Reach
for it when "keep going until something happens" reads more naturally than a while condition up top.
match - the star: handle every case, exhaustively
What it actually is. match compares a value against a list of patterns and runs the first one that
fits. It's like a switch from other languages, but with a superpower: it must cover every possible
case. Forget one and the program won't compile - sounds strict, but it's one of Rust's best
bug-prevention features.
$ cargo run
0 is zero
2 is small
7 is medium
100 is large
What just happened: Each arm (pattern => result) is checked top to bottom; the first match wins.
1 | 2 | 3 matches any of those values; 4..=9 matches the inclusive range 4 through 9; _ is the
catch-all. Like if, match is an expression, which is why classify can hand the whole match back
as its answer.
Here's the flow of evaluating a single value through that match:
Why "exhaustive" is a gift, not a chore
Say you have a type with a fixed set of options (an enum - a value that's exactly one of several named
variants), and you forget to handle one:
$ cargo run
error[E0004]: non-exhaustive patterns: `Light::Yellow` not covered
--> src/main.rs:4:11
|
4 | match l {
| ^ pattern `Light::Yellow` not covered
|
note: `Light` defined here
--> src/main.rs:1:6
|
1 | enum Light { Red, Yellow, Green }
| ^^^^^ ------ not covered
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
What just happened: The compiler refused to build because Light::Yellow isn't handled. In most
languages this would compile fine and quietly do nothing for yellow lights, a bug you'd find in production.
Rust catches it at compile time and names exactly which case you missed.
💡 Key point. Exhaustive match means: add a new variant to an enum later, and the compiler walks you
to every match that now needs updating. "Handle all the cases" stops being something you remember - the
compiler remembers for you. Why refactoring Rust feels safe.
⚠️ Gotcha - _ can hide bugs. The catch-all _ also satisfies exhaustiveness. Perfect for
genuinely-infinite types like i32 (as in classify), but on an enum it switches off the helpful "you
forgot a case" check. So on enums, prefer listing variants explicitly when you reasonably can - you want
the compiler to nag you when a new variant appears.
Functions - and returning without return
What it actually is. A function is a named, reusable block of logic that takes inputs (parameters) and
optionally produces an output. Define one with fn, name the parameter types (always required), and name
the return type after ->.
The Rust-flavored part: a function returns the value of its last expression, which has no semicolon.
$ cargo run
square of 5 is 25
What just happened: square takes an i32 named x and returns an i32 (-> i32). Its body is the
single expression x * x - no semicolon - so that value becomes the return value. You don't need to
write return x * x;, though you can; return exists mainly for returning early mid-function.
⚠️ The semicolon gotcha that bites everyone once. A semicolon turns an expression into a statement,
which produces no value. Writing x * x; as the last line means the function returns "nothing" - and if
you promised an i32, the compiler stops you with a "mismatched types" error, expecting i32 but finding
() (Rust's "no value," the unit type). The fix is almost always: delete the trailing semicolon.
Once "last line, no semicolon = the return value" clicks, this stops happening.
📝 Terminology. () is the unit type - Rust's way of saying "no meaningful value." A function with
no -> Type returns () (it's run for its side effects, like printing).
Recap
if/elseis an expression - it produces a value you can assign; there's no ternary becauseifcovers it.for ... inwalks collections and ranges (0..3excludes the end;0..=3includes it);whileloops on a condition;loopruns untilbreak.matchcompares a value to patterns and is exhaustive - forget a case and it won't compile. Likeif, it's an expression that produces a value.- Exhaustiveness is a gift: add an enum variant and the compiler points you to every
matchto fix. - Functions use
fn, require parameter types, and return after->. The last expression with no semicolon is the return value - a stray semicolon there is the classic beginner bug.
You can now branch, loop, and factor logic into functions. As programs grow past one file, you'll need a way to organize them - that's next: modules, crates, and how a real Rust project is laid out. Right after that comes the phase everything has been building toward: ownership.
← Phase 3: Collections · Guide overview · Phase 5: Modules & Project Layout →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. Why does Rust have no ternary `? :` operator?
2. What makes `match` special in Rust?
3. How does a Rust function return its value?