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 to bool. 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 if is optional and may be repeated as many times as needed to test more conditions.
  • else is optional when the if is 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

  1. Rust evaluates the first condition. It must type-check as bool — if it’s any other type, compilation fails before your program ever runs.
  2. If that condition is true, Rust runs the associated block and then jumps to the code after the entire if/else chain. No other branch is evaluated.
  3. If it’s false, Rust moves to the next else if condition (if any) and repeats the check.
  4. If none of the if/else if conditions are true and an else exists, its block runs.
  5. If the if/else is 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 an else exists — 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 declaring x as mut and reassigning it inside each branch — it’s shorter and the compiler guarantees x is always initialized.
  • Keep conditions readable: if a condition grows into several &&/|| clauses, pull it into a well-named bool variable or a small function first.
  • Reach for match instead of a long if/else if chain 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 let rather than if plus manual unwrapping when you only care about one variant of an Option or Result.
  • 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/else is 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 i32 variable named age. Using if/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 for age.
  • Write a function fn max(a: i32, b: i32) -> i32 that returns the larger of two numbers using a single if/else expression (no helper methods). Call it from main with a few pairs and print the results.
  • Write a program that stores a boolean is_raining and a boolean has_umbrella. Using &&, ||, and ! inside your conditions, print "stay dry", "bring an umbrella", or "you'll get wet" depending on the combination. Expected output for is_raining = true, has_umbrella = false is "you'll get wet".

Summary

  • if runs a block only when its condition is true; else if chains let you test further conditions, and else catches everything else.
  • Conditions must be a real bool — Rust has no truthy/falsy coercion from numbers, strings, or pointers.
  • if is an expression: the last expression (no semicolon) in each branch becomes the value of the whole construct, which you can assign with let or return from a function.
  • When used as an expression, every branch must produce the same type, and an else is 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 match over a long if/else if chain.