| |

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

FormExample
Basic ifif cond { }
If-elseif cond { } else { }
Else if chainif c1 { } else if c2 { } else { }
If expressionlet x = if cond { a } else { b };
If in argumentprintln!("{}", if c { "a" } else { "b" });

Condition Rules

RuleDetail
Must be boolNo truthy or falsy values
No coercionInteger cannot be used as condition
No null checkUse Option::is_some() or match
Braces requiredNo single-statement form

Branch Rules

SituationRule
Both branches presentMust produce same type
Used as expressionBoth branches required
Used as statementelse optional
Without elseType is ()

If vs Match

Use if whenUse match when
Two or three simple branchesMany branches
Conditions are not equalityMatching on value or pattern
Branch types differ in logicExhaustiveness matters
Boolean conditionsDestructuring 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

PitfallWhy It HappensFix
expected bool, found integerCondition is not a booleanUse count > 0 or count != 0
if without else has type ()Used in value positionAdd else
mismatched types in branchesBranches produce different typesMake types match
Unnecessary parentheses warningClippy style checkRemove parens around condition
Nested if readabilityDeeply nested conditionsUse else if or match
Missing braces errorSingle statement without blockAdd 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

ItemValue
if keywordif condition { }
Condition typeMust be bool
BracesRequired
elseOptional; executes when condition is false
else ifChain of conditions, evaluated in order
if as expressionProduces value when both branches present
Branch typesMust match when used as expression
Without elseType is (), statement only
Nested ifFlatten with else if or guard clauses
if letPattern matching shorthand for Option/Result

Key takeaways:

  • if is 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 count is an error; if count > 0 is correct.
  • Both branches must produce the same type when if is used as an expression. A mismatch is a compile error that names the two types.
  • An if without else has type (). It can be used as a statement but not as a value. Adding else makes 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 if chains evaluate conditions in order. The first true condition executes, and the rest are skipped. Long chains are a sign that match would be clearer.
  • if expressions can be nested inside other expressions. let max = if a > b { a } else { b }; and println!("{}", if x { "a" } else { "b" }); are both valid.
  • Flatten nested conditionals with else if or guard clauses. Deeply nested ifs 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!