Nested Conditions
A nested condition is an if expression placed inside the block of another if or else. The inner if is evaluated only when execution reaches the block that contains it.
fn main() {
let age = 20;
let has_ticket = true;
if age >= 18 {
if has_ticket {
println!("Welcome in.");
} else {
println!("Buy a ticket first.");
}
} else {
println!("Adults only.");
}
}
Welcome in.
+----------------+
| age >= 18 |
+--------+-------+
|
+--------+--------+
true false
| |
+-------v-------+ "Adults only."
| has_ticket |
+-------+-------+
|
+------+------+
true false
| |
"Welcome in." "Buy a ticket first."
The inner if runs only when age >= 18 is true. When age is below 18, has_ticket is never examined.
Where Nesting Can Appear
An if can be nested in the if block, in an else block, or in both. Any depth is allowed.
fn classify(a: u32, b: u32, c: u32) -> &'static str {
if a + b > c && a + c > b && b + c > a {
if a == b && b == c {
"equilateral"
} else if a == b || b == c || a == c {
"isosceles"
} else {
"scalene"
}
} else {
"not a triangle"
}
}
fn main() {
println!("{}", classify(3, 3, 3));
println!("{}", classify(3, 3, 5));
println!("{}", classify(3, 4, 5));
println!("{}", classify(1, 2, 8));
}
equilateral
isosceles
scalene
not a triangle
The outer if decides whether the three sides can form a triangle at all. Only then does the inner chain decide which kind of triangle it is. The inner chain is the value of the outer if block, so the whole expression is the function's return value.
Scope in Nested Blocks
An inner block can read every variable declared in the blocks that contain it. The reverse is not true: a variable declared in an inner block is dropped when that block ends.
fn main() {
let outer = 10;
if outer > 5 {
let inner = outer * 2;
if inner > 15 {
println!("outer = {outer}, inner = {inner}");
}
}
}
outer = 10, inner = 20
// ❌ does not compile
fn main() {
let outer = 10;
if outer > 5 {
let inner = outer * 2;
}
println!("{inner}");
}
error[E0425]: cannot find value `inner` in this scope
--> main.rs:7:16
|
7 | println!("{inner}");
| ^^^^^
|
help: the binding `inner` is available in a different scope in the same function
--> main.rs:5:13
|
5 | let inner = outer * 2;
| ^^^^^
Braces Decide Which if an else Belongs To
In some languages, an else after two nested if statements without braces is ambiguous: it is unclear which if it belongs to. Rust requires braces, so the structure is always explicit. An else belongs to the if whose closing brace it follows.
fn main() {
let a = true;
let b = false;
if a {
if b {
println!("a and b");
} else {
println!("a but not b");
}
}
if a {
if b {
println!("a and b");
}
} else {
println!("not a");
}
}
a but not b
In the first statement the else is attached to the inner if. In the second statement it is attached to the outer if. With a = true the second statement prints nothing, because the inner if has no else and the outer else is skipped.
Nested if Is Not the Same as &&
Nesting expresses "check B only after A holds". For two conditions with no else branches, that is equivalent to A && B, because && also skips B when A is false.
fn main() {
let x = 4;
if x > 0 {
if x % 2 == 0 {
println!("positive even");
}
}
if x > 0 && x % 2 == 0 {
println!("positive even (flat)");
}
}
positive even
positive even (flat)
Once an else is involved, the two forms are no longer interchangeable, because else on a combined condition covers every way the condition can fail. This function has three outcomes:
fn nested(logged_in: bool, is_admin: bool) -> &'static str {
if logged_in {
if is_admin {
"admin panel"
} else {
"dashboard"
}
} else {
"login page"
}
}
fn main() {
println!("{}", nested(true, true));
println!("{}", nested(true, false));
println!("{}", nested(false, true));
println!("{}", nested(false, false));
}
admin panel
dashboard
login page
login page
Combining the conditions with && and a single else would give only two outcomes. Keeping three outcomes with a flat shape needs an else if chain.
Flattening Nested Conditions
Each level of nesting pushes the code further right and makes the logic harder to follow. Deep nesting is a signal to restructure. There are four common ways.
Combine Conditions with &&
When all the checks lead to the same block, join them:
if a { if a && b && c {
if b { ...
if c { => }
...
}
}
}
Use an else if Chain
When nesting selects between several outcomes, a chain with combined conditions can express the same decisions at one level:
fn flat(logged_in: bool, is_admin: bool) -> &'static str {
if logged_in && is_admin {
"admin panel"
} else if logged_in {
"dashboard"
} else {
"login page"
}
}
fn main() {
println!("{}", flat(true, true));
println!("{}", flat(true, false));
println!("{}", flat(false, true));
}
admin panel
dashboard
login page
Match on a Tuple
When the outcome depends on a combination of values, match on a tuple lists every combination explicitly, and the compiler checks that none is missing:
fn by_tuple(logged_in: bool, is_admin: bool) -> &'static str {
match (logged_in, is_admin) {
(true, true) => "admin panel",
(true, false) => "dashboard",
(false, _) => "login page",
}
}
fn main() {
for (logged_in, is_admin) in [(true, true), (true, false), (false, true), (false, false)] {
println!("{logged_in:5} {is_admin:5} -> {}", by_tuple(logged_in, is_admin));
}
}
true true -> admin panel
true false -> dashboard
false true -> login page
false false -> login page
Return Early (Guard Clauses)
A guard clause checks for an invalid or special case at the top of a function and returns right away. The rest of the function then runs at a single indentation level. This is the most effective cure for deeply nested validation code.
The nested version below checks three rules, each inside the previous if. The guarded version checks the same rules with an early return for each failure.
fn validate_nested(username: &str) -> Result<(), &'static str> {
if !username.is_empty() {
if username.len() <= 12 {
if username.chars().all(|c| c.is_ascii_alphanumeric()) {
Ok(())
} else {
Err("only letters and digits are allowed")
}
} else {
Err("too long")
}
} else {
Err("empty")
}
}
fn validate_guarded(username: &str) -> Result<(), &'static str> {
if username.is_empty() {
return Err("empty");
}
if username.len() > 12 {
return Err("too long");
}
if !username.chars().all(|c| c.is_ascii_alphanumeric()) {
return Err("only letters and digits are allowed");
}
Ok(())
}
fn main() {
for name in ["", "ferris", "a_very_long_username", "no spaces"] {
println!("{name:?}: {:?} / {:?}", validate_nested(name), validate_guarded(name));
}
}
"": Err("empty") / Err("empty")
"ferris": Ok(()) / Ok(())
"a_very_long_username": Err("too long") / Err("too long")
"no spaces": Err("only letters and digits are allowed") / Err("only letters and digits are allowed")
In the guarded version each failure sits next to the check that detects it, and the successful path is the last line instead of the innermost block.
Nested Pattern Checks with Let Chains
Checking whether a value matches a pattern and then testing a property of what was extracted usually produces nested if let and if blocks. In the 2024 edition, a let chain joins them into one condition with &&:
fn main() {
let input = "42";
let limit = 100;
if let Ok(value) = input.parse::<i32>() {
if value > 0 {
if value < limit {
println!("{value} is in range");
}
}
}
if let Ok(value) = input.parse::<i32>()
&& value > 0
&& value < limit
{
println!("{value} is in range (flat)");
}
}
42 is in range
42 is in range (flat)
Let chains work only in if and while conditions and only in the 2024 edition. Pattern-based conditions are covered in detail in their own section.