if and else
The if and else keywords let a Rust program branch: run one block of code when a condition holds, and a different block (or none at all) when it doesn’t. If you’ve written conditionals in any other language, the shape will feel familiar — but Rust adds two rules that catch newcomers off guard. First, the condition must be a genuine bool; there is no “truthy” integer or pointer standing in for one. Second, if is an expression, not just a statement, so it can produce a value you assign directly to a variable.
Overview: How if and else Work
An if starts with the keyword if, a condition, and a block of code in curly braces. When the condition evaluates to true, Rust runs that block and skips the rest. You can chain as many else if clauses as you need to test additional conditions, and finish with a plain else that runs only when none of the earlier conditions matched. Conditions are checked top to bottom, and the first one that evaluates to true wins — later branches, even ones that would also be true, are never reached.
Unlike C, C++, JavaScript, or Python, Rust will not let you use an integer, a string, a pointer, or any other type as a stand-in for a boolean. Writing if number { ... } where number is an integer is a compile-time type error, not a runtime surprise where zero happens to mean “false.” You must write an actual boolean expression, such as number != 0. This is a deliberate design choice: it removes an entire category of bugs where a typo like = instead of ==, or an accidentally nonzero value, silently changes which branch runs.
The second surprise for newcomers is that if is an expression that can produce a value, not merely a statement that controls which code runs. In most C-family languages, an if never has a value of its own; you declare a variable first and assign it inside each branch. In Rust, the last expression in each branch’s block — the one with no trailing semicolon — becomes the value of the whole if/else. That lets you write let x = if condition { 1 } else { 2 }; in one step. Because the compiler must know the type of x at compile time, every branch of an if used this way has to produce the same type. Mixing an integer branch with a string branch, or omitting the else when a value is expected, is rejected at compile time. When you don’t need a value — an if used purely for a side effect like printing — you simply don’t assign the result, and else becomes optional.
Syntax
if condition1 {
// runs if condition1 is true
} else if condition2 {
// runs if condition1 is false and condition2 is true
} else {
// runs if none of the above conditions are true
}
condition1,condition2— any expression that evaluates tobool. No parentheses are required around the condition, and idiomatic Rust omits them.- Curly braces are mandatory around every branch, even a single-statement one — Rust has no “single-line if” without braces.
else ifis optional and may be repeated as many times as needed to test more conditions.elseis optional when theifis used purely as a statement, but required when its result is assigned to something, so every possible path produces a value.
Conditions are built from comparison and logical operators:
| Operator | Meaning |
|---|---|
== |
equal to |
!= |
not equal to |
< |
less than |
> |
greater than |
<= |
less than or equal to |
>= |
greater than or equal to |
&& |
logical AND; short-circuits, so the right side isn’t evaluated if the left side is already false |
|| |
logical OR; short-circuits, so the right side isn’t evaluated if the left side is already true |
! |
logical NOT; flips a bool |
Examples
Example 1: Basic if / else if / else
fn main() {
let number = 7;
if number > 0 {
println!("{} is positive", number);
} else if number < 0 {
println!("{} is negative", number);
} else {
println!("{} is zero", number);
}
}
Output:
7 is positive
Rust checks number > 0 first. Since 7 > 0 is true, it runs that branch and skips the else if and else entirely — they are never even evaluated once an earlier branch matches.
Example 2: if as an expression
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {}", number);
let temperature = 30;
let description = if temperature > 25 {
"hot"
} else if temperature > 15 {
"warm"
} else {
"cold"
};
println!("It's {} today", description);
}
Output:
The value of number is: 5
It's hot today
Both number and description are set directly from the value the if/else produces — there’s no intermediate mut variable that gets reassigned inside each branch. Notice that every branch of description produces a &str; if one branch returned a number instead, the compiler would reject the program because it wouldn’t know a single type for description.
Example 3: A realistic example — grading scores
fn letter_grade(score: u32) -> &'static str {
if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 70 {
"C"
} else if score >= 60 {
"D"
} else {
"F"
}
}
fn main() {
let scores: [u32; 4] = [95, 82, 58, 71];
for &score in scores.iter() {
println!("Score {} => Grade {}", score, letter_grade(score));
}
}
Output:
Score 95 => Grade A
Score 82 => Grade B
Score 58 => Grade F
Score 71 => Grade C
letter_grade takes a u32 and returns a borrowed string slice with a 'static lifetime, because each branch returns a string literal that’s baked into the compiled binary and lives for the whole program. The function is just an if/else if chain used as an expression: whichever branch matches becomes the function’s return value, with no explicit return keyword needed.
How It Works Step by Step
- Rust evaluates the first condition. It must type-check as
bool— if it’s any other type, compilation fails before your program ever runs. - If that condition is
true, Rust runs the associated block and then jumps to the code after the entireif/elsechain. No other branch is evaluated. - If it’s
false, Rust moves to the nextelse ifcondition (if any) and repeats the check. - If none of the
if/else ifconditions are true and anelseexists, its block runs. - If the
if/elseis used as an expression (its value is assigned or returned), the compiler additionally verifies that every branch’s final expression has the same type, and that anelseexists — without one, the “missing” branch is treated as producing(), which mismatches almost any real value.
Logical operators inside a condition are also evaluated with short-circuiting. In age >= 18 && has_id, Rust checks age >= 18 first; if that’s false, it never bothers evaluating has_id at all, because the whole expression can’t be true either way. This matters when the right-hand side has a side effect, like calling a function — with && and ||, that call may or may not happen depending on the left side.
let age = 20;
let has_id = true;
if age >= 18 && has_id {
println!("Entry allowed");
} else {
println!("Entry denied");
}
Output:
Entry allowed
Common Mistakes
Mistake 1: Using a non-boolean value as a condition
Coming from C, JavaScript, or Python, it’s tempting to treat a nonzero number as “true”:
fn main() {
let number = 3;
if number {
println!("number is truthy");
}
}
This fails to compile with an error like expected bool, found integer. Rust has no implicit conversion from numbers (or any other type) to bool. Write an explicit comparison instead:
let number = 3;
if number != 0 {
println!("number is not zero");
}
Output:
number is not zero
Mistake 2: Branches of an if expression with different types
When you use if/else to produce a value, every branch must resolve to the same type:
fn main() {
let condition = true;
let value = if condition {
5
} else {
"six"
};
println!("{}", value);
}
The compiler rejects this with if and else have incompatible types, because one branch is an integer and the other is a &str. Make the types agree — here, by converting both to an owned String:
let condition = true;
let value = if condition {
"5".to_string()
} else {
"six".to_string()
};
println!("{}", value);
Output:
5
Mistake 3: Forgetting else when the result is used as a value
fn main() {
let condition = true;
let number = if condition {
5
};
println!("{}", number);
}
Without an else, the “missing” path implicitly produces () (the unit type), so the compiler complains that the if may be missing an else clause and that it expected an integer but found (). Whenever you assign the result of an if, give it a complete else:
let condition = true;
let number = if condition {
5
} else {
0
};
println!("{}", number);
Output:
5
Best Practices
- Prefer
let x = if condition { a } else { b };over declaringxasmutand reassigning it inside each branch — it’s shorter and the compiler guaranteesxis always initialized. - Keep conditions readable: if a condition grows into several
&&/||clauses, pull it into a well-namedboolvariable or a small function first. - Reach for
matchinstead of a longif/else ifchain once you’re comparing one value against many discrete possibilities — it’s often clearer and the compiler checks you’ve covered every case. - Use
if letrather thanifplus manual unwrapping when you only care about one variant of anOptionorResult. - Don’t parenthesize conditions out of habit from C-family languages —
if (x > 0)compiles but idiomatic Rust omits the parentheses:if x > 0. - When an
if/elseis only run for its side effects, don’t force it to return a value; let each branch end in a statement (with a semicolon) rather than shoehorning in a shared type.
Practice Exercises
- Write a program with an
i32variable namedage. Usingif/else if/else, print"child"for ages under 13,"teenager"for 13 up to (but not including) 20, and"adult"otherwise. Try it with a few different values forage. - Write a function
fn max(a: i32, b: i32) -> i32that returns the larger of two numbers using a singleif/elseexpression (no helper methods). Call it frommainwith a few pairs and print the results. - Write a program that stores a boolean
is_rainingand a booleanhas_umbrella. Using&&,||, and!inside your conditions, print"stay dry","bring an umbrella", or"you'll get wet"depending on the combination. Expected output foris_raining = true, has_umbrella = falseis"you'll get wet".
Summary
ifruns a block only when its condition istrue;else ifchains let you test further conditions, andelsecatches everything else.- Conditions must be a real
bool— Rust has no truthy/falsy coercion from numbers, strings, or pointers. ifis an expression: the last expression (no semicolon) in each branch becomes the value of the whole construct, which you can assign withletor return from a function.- When used as an expression, every branch must produce the same type, and an
elseis required — otherwise the compiler rejects the program at compile time, not at runtime. &&and||short-circuit, so the right-hand side may never be evaluated.- For many discrete cases against a single value, prefer
matchover a longif/else ifchain.
