Why this matters
Up to now every program ran the same statements in the same order regardless of input. Real programs make decisions: reject bad input, choose a message, take a shortcut. if is the basic decision-maker. In Rust it is also an expression that produces a value, which lets you write decisions compactly and makes the compiler check that every path produces something sensible.
The basic form
fn main() {
let temperature = 31;
if temperature > 30 {
println!("hot");
} else if temperature < 10 {
println!("cold");
} else {
println!("pleasant");
}
println!("done");
}
hot
done
- The condition follows
ifwith no parentheses (you may write them, but the compiler will warn that they are unnecessary). - The braces are mandatory, even for a single statement. This avoids the classic C bug where an indented second line silently falls outside the
if. else ifchains test conditions in order; the first true one wins and the rest are skipped.elseruns when nothing above matched. It is optional.
The condition must be a bool. Rust has no "truthy" values:
fn main() {
let count = 3;
if count {
println!("non-zero");
}
}
error[E0308]: mismatched types
--> main.rs:3:8
|
3 | if count {
| ^^^^^ expected `bool`, found integer
Write the comparison you mean: if count != 0.
What the CPU does
An if compiles to a comparison and a conditional jump:
compare temperature with 30
jump-if-not-greater to ELSE_IF
... code for "hot" ...
jump to END
ELSE_IF:
compare temperature with 10
jump-if-not-less to ELSE
... code for "cold" ...
jump to END
ELSE:
... code for "pleasant" ...
END:
... println!("done") ...
The CPU executes instructions in sequence until a jump tells it to continue elsewhere. Every decision, loop and function call you write ends up as jumps like these.
if is an expression
Because if is an expression, it has a value: the value of whichever block ran.
fn main() {
let n = 7;
let parity = if n % 2 == 0 { "even" } else { "odd" };
println!("{n} is {parity}");
let fee = if n >= 5 { 0 } else { 2 } + 1;
println!("fee {fee}");
}
7 is odd
fee 1
This replaces the ternary operator cond ? a : b that C and Java have; Rust does not need one. Note the ; after the closing brace when an if is part of a let: the let statement ends there.
It also makes functions read cleanly. The body of this function is a single expression:
fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
fn main() {
println!("{}", max(3, 9));
}
9
Both branches must agree
An if used as a value must produce the same type from every branch, because the variable it initializes has a single type:
fn main() {
let n = 3;
let label = if n % 2 == 0 { "even" } else { 1 };
println!("{label}");
}
error[E0308]: `if` and `else` have incompatible types
--> main.rs:3:49
|
3 | let label = if n % 2 == 0 { "even" } else { 1 };
| ------ ^ expected `&str`, found integer
| |
| expected because of this
And it must have an else: without one, the if produces nothing when the condition is false.
fn main() {
let n = 3;
let label = if n > 0 { "positive" };
println!("{label}");
}
error[E0317]: `if` may be missing an `else` clause
--> main.rs:3:17
|
3 | let label = if n > 0 { "positive" };
| ^^^^^^^^^^^----------^^
| | |
| | found here
| expected `&str`, found `()`
|
= note: `if` expressions without `else` evaluate to `()`
= help: consider adding an `else` block that evaluates to the expected type
E0317: an if without else has type () when the condition is false, which does not match &str. The compiler checks every path. This is a pattern you will see throughout Rust: things that would be a run-time "undefined" or "null" in other languages are caught at compile time because the compiler insists every case produces a proper value.
An if used as a statement (no value wanted) can omit else; then both branches have type () anyway.
Writing good conditions
Combine tests with &&, || and !, and remember short-circuiting from the previous module:
fn main() {
let divisor = 0;
let total = 10;
if divisor != 0 && total / divisor > 2 {
println!("big ratio");
} else {
println!("no ratio");
}
}
no ratio
Because divisor != 0 is false, the division is never evaluated, so there is no divide-by-zero panic.
Tips:
- Order
else ifchains from most specific to least. In a grade calculator, test>= 90before>= 80; the other way round, everyone above 80 would get a B. - Handle invalid cases first and keep the main logic unindented. With early
return, a function can reject bad values at the top. - Prefer positive conditions when readable:
if is_validreads better thanif !is_invalid. - For many cases on one value,
match(lesson 4 of this module) is usually clearer than a longelse ifchain.
Common mistakes
Using = instead of ==. if x = 5 is an error in Rust (E0308: expected bool, found ()), whereas in C it silently assigns. Good news: Rust catches it.
A semicolon inside a value branch. let x = if c { 1; } else { 2 }; makes the first branch (), giving E0308 incompatible types. Value-producing blocks end without a semicolon.
Forgetting the ; after a let ... = if ... { } else { };. The compiler says "expected ;" at the end of the if.
Overlapping or misordered ranges in else if chains: the compiler cannot catch these; your tests must.
Recap
if cond { ... } else if cond { ... } else { ... }: braces required, condition must bebool.ifis an expression:let x = if c { a } else { b };.- As a value, both branches must have the same type, and
elseis required (E0308, E0317). - Conditions compile to comparisons and conditional jumps.
&&short-circuits, which lets a cheap guard protect an expensive or dangerous test.
Going further: rewrite a chain of three if/else if statements that each call println! into one let message = if ... expression followed by a single println!. Which version is easier to change later?