Polyglot
Languages Rust conditionals
Rust § conditionals

Conditionals

Rust’s conditional constructs are expressions, not statements: if/else produces a value, and match is a pattern-matching expression with substantial discriminating power. The if let and while let admit pattern-matching against a single pattern; the let else admits binding-with-fallback. The combination — if/else for boolean conditions, match for pattern dispatch, if let for one-arm pattern matching — covers the conditional surface; chained if/else if is admitted but conventionally replaced by match for non-trivial discrimination.

This page covers the principal conditional constructs; pattern matching as a separate feature is in Pattern matching.

if/else

The principal form:

if condition {
    // body
} else if other_condition {
    // body
} else {
    // body
}

The blocks are delimited by {}; each branch executes if its condition is true. The condition must be a bool — Rust does not coerce other types:

let n = 5;
if n {                                          // ERROR: expected bool, got integer
    // ...
}
if n != 0 {                                     // OK
    // ...
}

The strictness eliminates substantial classes of bugs that C-style truthiness admits.

if/else is an expression — it produces a value:

let max = if a > b { a } else { b };

The two arms must have the same type; the value of the if is the value of the selected arm. The else is required (otherwise the type would have to be ()).

For multi-branch logic:

let category = if n > 100 {
    "large"
} else if n > 10 {
    "medium"
} else {
    "small"
};

The chained form is admitted; for substantive discrimination, match is conventionally clearer.

match

The match expression admits pattern-matching against a value:

let n = 5;
match n {
    0 => println!("zero"),
    1 => println!("one"),
    2 | 3 => println!("two or three"),
    4..=10 => println!("four through ten"),
    _ => println!("other"),
}

The arms are tried in order; the first matching pattern’s body executes. The _ wildcard catches everything not covered.

match is exhaustive — every possible value of the discriminant must be covered, or the compiler refuses to compile:

enum Color { Red, Green, Blue }

let c = Color::Red;
match c {
    Color::Red => println!("red"),
    Color::Green => println!("green"),
    // ERROR: Color::Blue not covered
}

The exhaustiveness check is one of Rust’s most distinctive safety features; adding a new enum variant produces compile errors at every match that does not handle it.

For value-level matching with elaborate patterns, treated in Pattern matching.

match is also an expression:

let description = match shape {
    Shape::Circle(r) => format!("circle radius {}", r),
    Shape::Square(s) => format!("square side {}", s),
};

Each arm produces a value of the same type.

if let

if let admits one-arm pattern matching — useful when only one variant is interesting:

let some_value = Some(5);

if let Some(n) = some_value {
    println!("got {}", n);
}

// Equivalent to:
match some_value {
    Some(n) => println!("got {}", n),
    None => (),                                  // do nothing
}

The form is the conventional Rust idiom for “if it matches this pattern, do the body”.

if let may have an else:

if let Some(n) = some_value {
    println!("got {}", n);
} else {
    println!("got nothing");
}

For the negation — “if it does NOT match, do the body” — the let else form (since 1.65):

let Some(n) = some_value else {
    return Err("expected a value");
};

// n is now in scope; bound from the Some(n) pattern
println!("got {}", n);

The let else is the conventional Rust form for “early return on mismatch”; the body must not fall through (it must return, break, continue, or panic).

while

The while loop tests a condition before each iteration:

let mut n = 10;
while n > 0 {
    println!("{}", n);
    n -= 1;
}

The condition must be a bool. The conventional uses are polling and condition-driven loops.

For “loop with state and termination by some signal”, loop plus break is conventional.

while let

Similar to if let for loops:

let mut stack = vec![1, 2, 3];

while let Some(top) = stack.pop() {
    println!("{}", top);
}

The loop continues as long as the pattern matches; on mismatch, the loop exits.

loop

The loop keyword introduces an infinite loop:

loop {
    // ... some condition for break ...
    if done {
        break;
    }
}

The conventional uses are servers, event loops, retry loops, and any loop where termination depends on internal state rather than an upfront condition.

loop may return a value with break:

let result = loop {
    let attempt = try_compute();
    if let Ok(value) = attempt {
        break value;                            // returns from the loop
    }
};

The break value form admits using loop as a value-producing construct; useful for retry-with-result patterns.

for

The for loop iterates over an iterable:

for n in 0..10 {
    println!("{}", n);
}

let v = vec![1, 2, 3, 4];
for x in &v {                                   // borrow; v retains ownership
    println!("{}", x);
}

for x in v.iter() {                              // explicit iterator
    println!("{}", x);
}

for (i, x) in v.iter().enumerate() {
    println!("{}: {}", i, x);
}

Treated in Loops.

Labels for nested loops

Loops may be labelled; break and continue may target a specific label:

'outer: for i in 0..10 {
    for j in 0..10 {
        if i * j > 50 {
            break 'outer;                       // breaks the outer loop
        }
    }
}

'main_loop: loop {
    'inner: for n in 0..100 {
        if n == 42 {
            continue 'main_loop;                // restart the outer loop
        }
    }
}

The 'name: prefix introduces a label; break 'name and continue 'name admit targeting the specific loop. The mechanism is rare in routine code but useful for early exit from nested loops.

Selection idioms

Early return

fn process(input: &str) -> Result<i32, String> {
    if input.is_empty() {
        return Err("empty input".to_string());
    }
    if input.len() > 100 {
        return Err("input too long".to_string());
    }

    // main body
    Ok(input.len() as i32)
}

The pattern reduces nesting; the conventional Rust style favours early returns for precondition validation.

match with guards

let n = 5;
match n {
    n if n < 0 => println!("negative"),
    0 => println!("zero"),
    n if n < 10 => println!("small"),
    _ => println!("large"),
}

The n if cond admits a guard — a boolean condition that must hold for the arm to match. The combination of patterns and guards admits substantial discriminating power.

match for value mapping

fn classify(n: i32) -> &'static str {
    match n {
        0 => "zero",
        n if n > 0 => "positive",
        _ => "negative",
    }
}

The match expression’s value is returned; the conventional Rust form for “value-driven dispatch returning a value”.

if let for Option and Result

let result = compute();

if let Ok(value) = result {
    process(value);
}

if let Some(x) = optional {
    use_value(x);
}

The conventional “do something if the value is Ok/Some” pattern.

let else for required values

fn greet(name: Option<&str>) -> Result<(), &'static str> {
    let Some(name) = name else {
        return Err("name is required");
    };
    println!("Hello, {}", name);
    Ok(())
}

The pattern admits a binding-or-error in one expression.

Combined if/match

let action = match status {
    Status::Active if has_permissions => Action::Allow,
    Status::Active => Action::DenyNoPermission,
    Status::Inactive => Action::DenyInactive,
    Status::Banned => Action::DenyBanned,
};

The pattern combines value-level discrimination with boolean guards.

Truthiness

Rust does not have truthiness; if requires a bool:

if some_option {                               // ERROR: Option is not bool
}

if some_option.is_some() {                     // OK
}

if let Some(_) = some_option {                 // OK
}

The strictness produces clearer code; the conventional if let and the is_some()/is_none()/is_ok()/is_err() methods admit the conventional checks.

A note on the absence of switch

Rust does not have a C-style switch statement; match replaces it entirely. The principal advantages of match over switch:

  • Exhaustiveness checking — every variant must be covered.
  • Pattern matching — admits destructuring, ranges, guards.
  • Expression — produces a value.
  • No fallthrough — each arm is independent (no implicit “fall to next”).

The conventional Rust dispatch is match for sum types and value comparisons; if/else for boolean conditions.

? operator

The ? operator admits compact error propagation:

fn parse_two(s: &str, t: &str) -> Result<i32, ParseIntError> {
    let a: i32 = s.parse()?;
    let b: i32 = t.parse()?;
    Ok(a + b)
}

The ? is roughly:

let a: i32 = match s.parse() {
    Ok(n) => n,
    Err(e) => return Err(e),
};

The mechanism admits substantial conciseness in error-handling chains; treated in Error handling.

The ? works for Option<T> and Result<T, E> (and any type implementing the Try trait).

Common patterns

Validate-and-compute

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("division by zero".to_string());
    }
    Ok(a / b)
}

Chained if let

if let Some(n) = parse_int(input) {
    if let Ok(result) = compute(n) {
        println!("{}", result);
    }
}

// Or, with the let-else form:
let Some(n) = parse_int(input) else {
    return;
};
let Ok(result) = compute(n) else {
    return;
};
println!("{}", result);

The let else form is conventionally clearer than nested if let.

match on tuples

match (x, y) {
    (0, 0) => println!("origin"),
    (_, 0) => println!("x-axis"),
    (0, _) => println!("y-axis"),
    (x, y) if x == y => println!("diagonal"),
    _ => println!("elsewhere"),
}

The tuple pattern admits multi-axis discrimination.

match returning a value

let day_name = match weekday {
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    4 => "Thursday",
    5 => "Friday",
    6 => "Saturday",
    7 => "Sunday",
    _ => "invalid",
};

The match expression admits compact value-dispatch.

A note on the conventional discipline

The contemporary Rust conditional advice:

  • Use if/else for boolean conditions.
  • Use match for value-driven discrimination — especially over enums.
  • Use if let for one-arm pattern matching where only one variant matters.
  • Use let else for “bind or early return”.
  • Trust exhaustiveness checking — let the compiler verify that every variant is covered.

The combination — boolean if, pattern match, single-pattern if let, binding let else — admits a substantial discriminating surface; the discipline is to choose the right form for each case.