Rust 9 🦀 Control Flow with if, else if, and else
Control flow determines which code runs under which conditions. Rust’s if expression is the primary tool for branching, and it differs from the if of most languages in one important way: it is an expression, not a statement. This means it produces a value, and that value can be assigned to a variable, returned from a function, or passed as an argument. The if keyword, the condition in parentheses, and the block of code that follows are the same as in C-family languages, but the expression-oriented model gives them a different role.
The condition must be a bool. Rust does not coerce integers, pointers, or other values to boolean, unlike C or JavaScript. A condition like if count is a compile error; you must write if count != 0 or if count > 0. This strictness eliminates a class of bugs where a value that happens to be non-zero is treated as true or where a null pointer is treated as false.
This chapter covers if syntax, else if chains, else blocks, if as an expression, the requirement for bool conditions, nested conditionals, and the patterns that keep conditional logic readable.
Key point: if is an expression in Rust; it produces a value. The condition must be bool. When both branches are present, they must produce the same type, and the value of the taken branch becomes the value of the if. An if without else has type () when used as an expression.
Why if is an expression
The value-selection problem. A common pattern is to choose between two values based on a condition. In statement-oriented languages, this requires a temporary variable and an assignment in each branch. In Rust, the if expression produces the value directly, so the selection is a single expression that can be assigned or returned without an intermediate variable.
The ternary problem. Many languages have a separate ternary operator for conditional expressions and an if statement for conditional execution. Rust unifies them: if is both. There is no ternary operator because if already serves that purpose, and the syntax is consistent whether the result is used as a value or executed for its side effects.
The type-safety problem. Requiring a bool condition means the compiler can verify that the condition is actually a boolean. There is no implicit conversion from integers, no truthy or falsy values, and no null-as-false. This catches mistakes that would be silent in other languages.
The consistency problem. Because if is an expression, it composes with the rest of the language. An if can appear inside another if, inside a match, inside a block, and as the value of a let binding. There is no special case; if follows the same rules as every other expression.
The exhaustiveness problem. When an if has both branches and is used as an expression, both branches must produce the same type. The compiler verifies this, so a missing branch or a mismatched type is a compile error rather than a runtime surprise.
a. Basic if syntax
The if keyword is followed by a condition in parentheses and a block of code in braces. The block executes if the condition is true.
let number = 7;
if number > 5 {
println!("Greater than 5");
}
The condition must be a bool. Passing a non-boolean value is an error:
let count = 3;
// Error: expected bool, found integer
if count {
println!("has items");
}
The correct form compares the value to produce a bool:
if count > 0 {
println!("has items");
}
if count != 0 {
println!("non-zero");
}
if !items.is_empty() {
println!("not empty");
}
The block is required. Rust does not allow a single statement without braces:
// Error: expected block
if cond println!("yes");
This is a deliberate choice. Braces make the scope explicit and prevent the dangling-else problem that plagues languages with optional braces.
The condition does not need parentheses around it, though they are allowed:
if number > 5 { } // idiomatic
if (number > 5) { } // valid but unnecessary
Clippy, Rust’s linter, warns about unnecessary parentheses around conditions.
b. else if and else chains
An else block executes when the if condition is false. An else if chain evaluates each condition in order until one is true.
let score = 85;
if score >= 90 {
println!("A");
} else if score >= 80 {
println!("B");
} else if score >= 70 {
println!("C");
} else {
println!("F");
}
Each condition is evaluated in order. Once one matches, its block executes and the rest of the chain is skipped. If no condition matches and there is no else, nothing happens.
The else block is optional. An if without else executes the block when the condition is true and does nothing otherwise.
if is_valid {
process();
}
// if is_valid is false, nothing happens
Chains can be as long as needed, but long chains are a sign that a match would be clearer. When the conditions are all comparisons against the same value, match expresses the intent more directly:
// if/else if chain
if grade == 'A' {
println!("Excellent");
} else if grade == 'B' {
println!("Good");
} else {
println!("Other");
}
// match equivalent
match grade {
'A' => println!("Excellent"),
'B' => println!("Good"),
_ => println!("Other"),
}
c. if as an expression
Because if is an expression, it can produce a value. When both branches are present and produce the same type, the value of the taken branch is the value of the if.
let number = 7;
let description = if number > 5 { "large" } else { "small" };
println!("{}", description); // large
This replaces the ternary operator that other languages provide. There is no ? : in Rust; the if expression serves that role with clearer syntax.
Both branches must produce the same type. A mismatch is a compile error:
// Error: mismatched types
let x = if cond { 5 } else { "hello" };
The error names the two types and the position of the branches. The fix is to make both branches produce the same type, either by converting one or by using a match with a common type.
An if expression can be used in any value position:
let max = if a > b { a } else { b };
fn absolute(n: i32) -> i32 {
if n >= 0 { n } else { -n }
}
println!("{}", if active { "on" } else { "off" });
let fee = if is_member { 0.0 } else { 9.99 };
An if without else has type () because the missing branch would produce (). This means it cannot be used where a value is expected:
// Error: if without else has type ()
let x = if cond { 5 };
The statement form is fine:
if cond {
println!("yes");
}
// no value needed; the () type is fine as a statement
d. Nested conditionals
An if can appear inside another if, either as a statement or as an expression. Deeply nested ifs are hard to read, and Rust provides else if and match to reduce nesting.
// Nested if
if user.is_authenticated {
if user.is_admin {
grant_access();
} else {
grant_limited_access();
}
} else {
deny_access();
}
// Flattened with else if
if !user.is_authenticated {
deny_access();
} else if user.is_admin {
grant_access();
} else {
grant_limited_access();
}
The flattened version reads as a sequence of cases rather than a hierarchy. This is a common refactoring when nesting grows.
Guard clauses use early return to flatten nesting in functions:
fn process(user: Option<&User>) -> Result<(), Error> {
let user = match user {
Some(u) => u,
None => return Err(Error::NoUser),
};
if !user.is_active {
return Err(Error::Inactive);
}
// main logic at the top level
Ok(())
}
Each condition is checked once, and the happy path stays unindented.
Complete Example Session
// ============================================
// PART 1: BASIC IF
// ============================================
fn main() {
let number = 7;
if number > 5 {
println!("Greater than 5");
}
}
// ============================================
// PART 2: IF WITH ELSE
// ============================================
fn main() {
let number = 3;
if number > 5 {
println!("Greater than 5");
} else {
println!("Not greater than 5");
}
}
// ============================================
// PART 3: ELSE IF CHAIN
// ============================================
fn main() {
let score = 85;
if score >= 90 {
println!("A");
} else if score >= 80 {
println!("B");
} else if score >= 70 {
println!("C");
} else {
println!("F");
}
}
// ============================================
// PART 4: CONDITION MUST BE BOOL
// ============================================
fn main() {
let count = 3;
// Error: expected bool, found integer
// if count {
// println!("has items");
// }
if count > 0 {
println!("has items");
}
if count != 0 {
println!("non-zero");
}
}
// ============================================
// PART 5: IF AS EXPRESSION
// ============================================
fn main() {
let number = 7;
let description = if number > 5 { "large" } else { "small" };
println!("{}", description); // large
}
// ============================================
// PART 6: BOTH BRANCHES MUST MATCH
// ============================================
fn main() {
let cond = true;
// Error: mismatched types
// let x = if cond { 5 } else { "hello" };
let x = if cond { 5 } else { 0 };
println!("{}", x); // 5
}
// ============================================
// PART 7: IF IN FUNCTION RETURN
// ============================================
fn absolute(n: i32) -> i32 {
if n >= 0 {
n
} else {
-n
}
}
fn main() {
println!("{}", absolute(-42)); // 42
println!("{}", absolute(42)); // 42
}
// ============================================
// PART 8: IF WITHOUT ELSE IS ()
// ============================================
fn main() {
let cond = true;
// Error: if without else has type ()
// let x = if cond { 5 };
if cond {
println!("statement form is fine");
}
}
// ============================================
// PART 9: NESTED IF FLATTENED WITH ELSE IF
// ============================================
fn access(authenticated: bool, admin: bool) {
if !authenticated {
println!("denied");
} else if admin {
println!("full access");
} else {
println!("limited access");
}
}
fn main() {
access(false, false); // denied
access(true, true); // full access
access(true, false); // limited access
}
// ============================================
// PART 10: IF IN VALUE POSITION
// ============================================
fn main() {
let a = 10;
let b = 20;
let max = if a > b { a } else { b };
println!("max = {}", max); // 20
println!("{}", if a % 2 == 0 { "even" } else { "odd" }); // even
}
These ten parts cover basic if, else, else if chains, the boolean requirement, if as an expression, type matching in branches, if as a function return, the unit type of if without else, flattening nested conditionals, and if in value positions.
Quick Reference
If Syntax Forms
| Form | Example |
|---|---|
| Basic if | if cond { } |
| If-else | if cond { } else { } |
| Else if chain | if c1 { } else if c2 { } else { } |
| If expression | let x = if cond { a } else { b }; |
| If in argument | println!("{}", if c { "a" } else { "b" }); |
Condition Rules
| Rule | Detail |
|---|---|
Must be bool | No truthy or falsy values |
| No coercion | Integer cannot be used as condition |
| No null check | Use Option::is_some() or match |
| Braces required | No single-statement form |
Branch Rules
| Situation | Rule |
|---|---|
| Both branches present | Must produce same type |
| Used as expression | Both branches required |
| Used as statement | else optional |
Without else | Type is () |
If vs Match
Use if when | Use match when |
|---|---|
| Two or three simple branches | Many branches |
| Conditions are not equality | Matching on value or pattern |
| Branch types differ in logic | Exhaustiveness matters |
| Boolean conditions | Destructuring required |
Best Practices
✅ Do This:
// Use if expression for value selection
let max = if a > b { a } else { b };
// Use explicit boolean comparison
if count > 0 { }
if !items.is_empty() { }
// Flatten nesting with else if
if !auth { denied(); }
else if admin { full(); }
else { limited(); }
// Use guard clauses in functions
if input.is_empty() { return Err(...); }
❌ Don’t Do This:
// Non-boolean condition
if count { } // ❌ count is not bool
// If without else as expression
let x = if cond { 5 }; // ❌ type is ()
// Mismatched branch types
let x = if c { 5 } else { "a" }; // ❌ different types
// Deeply nested ifs
if a { if b { if c { } } } // ❌ use else if or match
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
expected bool, found integer | Condition is not a boolean | Use count > 0 or count != 0 |
if without else has type () | Used in value position | Add else |
mismatched types in branches | Branches produce different types | Make types match |
| Unnecessary parentheses warning | Clippy style check | Remove parens around condition |
| Nested if readability | Deeply nested conditions | Use else if or match |
| Missing braces error | Single statement without block | Add braces |
Real-World Examples
1. Value Selection
let label = if count == 0 { "empty" } else { "has items" };
2. Range Check
if age >= 18 && age < 65 {
println!("working age");
}
3. Guard Clause
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
return None;
}
Some(a / b)
}
4. Authentication Check
if !user.is_authenticated {
redirect_to_login();
} else if user.is_admin {
show_admin_panel();
} else {
show_dashboard();
}
5. Conditional Default
let port = if config.port > 0 { config.port } else { 8080 };
6. Empty Check
if items.is_empty() {
println!("no items");
}
7. Compound Condition
if is_weekend || is_holiday {
println!("day off");
}
8. Optional Value Check
if let Some(value) = optional {
println!("{}", value);
}
9. Early Exit in Loop
for item in items {
if item.is_invalid() {
continue;
}
process(item);
}
10. Absolute Value
let abs = if n < 0 { -n } else { n };
Visual
If as Statement vs Expression
┌──────────────────────────────────────────────────────────────┐
│ STATEMENT FORM EXPRESSION FORM │
│ │
│ if cond { let x = if cond { a } │
│ do_something(); else { b }; │
│ } │
│ │
│ No value produced. Produces a value. │
│ Used for side effects. Used for value selection. │
│ else optional. else required. │
└──────────────────────────────────────────────────────────────┘
Condition Must Be Bool
┌──────────────────────────────────────────────────────────────┐
│ RUST OTHER LANGUAGES │
│ │
│ if count > 0 { } if (count) { } │
│ if count != 0 { } if (count != 0) { } │
│ if !items.is_empty() { } if (items.length) { } │
│ │
│ Rust requires a bool. Truthy/falsy values are │
│ No implicit conversion. converted automatically. │
│ │
│ This catches bugs where an integer or pointer is │
│ accidentally used as a condition. │
└──────────────────────────────────────────────────────────────┘
Branch Type Matching
┌──────────────────────────────────────────────────────────────┐
│ let x = if cond { 5 } else { 10 }; ✅ both i32 │
│ let x = if cond { 5 } else { 0 }; ✅ both i32 │
│ let x = if cond { 5 } else { "a" }; ❌ i32 vs &str │
│ │
│ When if is used as an expression, both branches must │
│ produce the same type. The compiler reports the mismatch. │
└──────────────────────────────────────────────────────────────┘
Flattening Nested Ifs
┌──────────────────────────────────────────────────────────────┐
│ NESTED FLATTENED │
│ │
│ if auth { if !auth { │
│ if admin { denied(); │
│ full(); } else if admin { │
│ } else { full(); │
│ limited(); } else { │
│ } limited(); │
│ } else { } │
│ denied(); │
│ } │
│ │
│ Same logic. The flattened form reads as a sequence │
│ of cases rather than a hierarchy. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
if keyword | if condition { } |
| Condition type | Must be bool |
| Braces | Required |
else | Optional; executes when condition is false |
else if | Chain of conditions, evaluated in order |
if as expression | Produces value when both branches present |
| Branch types | Must match when used as expression |
Without else | Type is (), statement only |
Nested if | Flatten with else if or guard clauses |
if let | Pattern matching shorthand for Option/Result |
Key takeaways:
ifis an expression in Rust, not a statement. It produces a value when both branches are present, and that value can be assigned, returned, or passed as an argument.- The condition must be a
bool. Rust does not convert integers, pointers, or other types to boolean.if countis an error;if count > 0is correct. - Both branches must produce the same type when
ifis used as an expression. A mismatch is a compile error that names the two types. - An
ifwithoutelsehas type(). It can be used as a statement but not as a value. Addingelsemakes it produce a value. - Braces are always required. Rust does not allow a single statement without a block, which eliminates the dangling-else problem.
else ifchains evaluate conditions in order. The first true condition executes, and the rest are skipped. Long chains are a sign thatmatchwould be clearer.ifexpressions can be nested inside other expressions.let max = if a > b { a } else { b };andprintln!("{}", if x { "a" } else { "b" });are both valid.- Flatten nested conditionals with
else ifor guard clauses. Deeply nestedifs are hard to read; reordering the conditions keeps the happy path at the top level.
Remember: The if expression is the fundamental branching construct in Rust. Its condition must be a boolean, and its branches must produce the same type when the result is used as a value. Because if is an expression, it replaces the ternary operator of other languages: if cond { a } else { b } is the idiomatic way to select between two values. An if without else has type (), so it works as a statement but not as a value. Keep conditions simple, use else if for multiple cases, and switch to match when the branching becomes complex. The expression-oriented design means if composes with the rest of the language, appearing inside assignments, function calls, and other expressions without special syntax.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!