Control Flow

if as an Expression

In Rust, if is an expression, not only a statement. Every if-else evaluates to a value: the value of the block that was selected. That value can be stored in a variable, returned from a function, passed as an argument, or used as an operand.

fn main() {
    let n = 14;
    let size = if n > 10 { "big" } else { "small" };
    println!("{n} is {size}");
}
14 is big

Rust has no ternary operator (condition ? a : b). if-else fills that role. The ? operator in Rust is used for error propagation and is unrelated.

C / Java / JavaScript Rust
int size = n > 10 ? 1 : 0; let size = if n > 10 { 1 } else { 0 };

How the Value Is Produced

A block evaluates to its final expression, the last line without a trailing semicolon. The if expression takes the value of whichever block runs.

let size = if n > 10 { "big" } else { "small" };
                         ^^^^^          ^^^^^^^
                         value if       value if
                         true           false

Blocks can contain statements before the final expression. Only the final expression becomes the value:

fn main() {
    let items = 3;
    let total = if items > 0 {
        let unit_price = 12;
        let discount = if items >= 3 { 5 } else { 0 };
        items * unit_price - discount
    } else {
        0
    };
    println!("total = {total}");
}
total = 31

The if here is itself used as a value inside the block (discount). else if chains also produce values, since else if is an else block containing another if expression:

fn main() {
    let a = 12;
    let b = 7;
    let larger = if a > b { a } else { b };
    println!("larger of {a} and {b} is {larger}");
    println!("max method: {}", a.max(b));

    let n = -4;
    let sign = if n < 0 {
        -1
    } else if n == 0 {
        0
    } else {
        1
    };
    println!("sign of {n} is {sign}");
}
larger of 12 and 7 is 12
max method: 12
sign of -4 is -1

Where an if Expression Can Appear

An if expression can be used anywhere an expression of its type is allowed.

struct Config {
    verbose: bool,
    retries: u32,
}

fn describe(is_member: bool) -> &'static str {
    if is_member { "member" } else { "guest" }
}

fn main() {
    let debug = true;
    let config = Config {
        verbose: debug,
        retries: if debug { 1 } else { 5 },
    };
    println!("verbose = {}, retries = {}", config.verbose, config.retries);

    let base = 100;
    let is_member = true;
    let price = base + if is_member { -20 } else { 0 };
    println!("price = {price}");

    println!("hello, {}", describe(is_member));
    println!("count: {}", if debug { "few" } else { "many" });
    println!("score = {}", 10 * if debug { 2 } else { 3 });
}
verbose = true, retries = 1
price = 80
hello, member
count: few
score = 20
Position Example
Right side of let let x = if c { 1 } else { 2 };
Struct field value retries: if debug { 1 } else { 5 }
Operand base + if is_member { -20 } else { 0 }
Function or macro argument println!("{}", if c { "a" } else { "b" })
Function body (tail expression) fn f(c: bool) -> i32 { if c { 1 } else { 2 } }

Both Branches Must Have the Same Type

An if expression has exactly one type, so the if block and the else block must evaluate to the same type. The compiler checks this at compile time.

// ❌ does not compile
fn main() {
    let ready = true;
    let value = if ready { 5 } else { "five" };
    println!("{value}");
}
error[E0308]: `if` and `else` have incompatible types
 --> main.rs:3:39
  |
3 |     let value = if ready { 5 } else { "five" };
  |                            -          ^^^^^^ expected integer, found `&str`
  |                            |
  |                            expected because of this

The same rule applies to near misses. An integer and a float are different types, and so are &str and String:

// ❌ does not compile
fn main() {
    let ready = true;
    let c = if ready { 1 } else { 2.5 };
    println!("{c}");
}
error[E0308]: `if` and `else` have incompatible types
 --> main.rs:4:35
  |
4 |     let c = if ready { 1 } else { 2.5 };
  |                        -          ^^^ expected integer, found floating-point number
  |                        |
  |                        expected because of this
// ❌ does not compile
fn main() {
    let ready = true;
    let a = "literal";
    let b = String::from("owned");
    let value = if ready { a } else { b };
    println!("{value}");
}
error[E0308]: `if` and `else` have incompatible types
 --> main.rs:5:39
  |
5 |     let value = if ready { a } else { b };
  |                            -          ^ expected `&str`, found `String`
  |                            |
  |                            expected because of this
  |
help: consider borrowing here
  |
5 |     let value = if ready { a } else { &b };
  |                                       +

Convert one branch so both have the same type:

fn main() {
    let ready = false;
    let a = "literal";
    let b = String::from("owned");
    let value = if ready { a.to_string() } else { b };
    println!("{value}");
}
owned

Untyped literals adapt to the other branch. In if ready { 1 } else { a } where a is a u8, the literal 1 becomes a u8:

fn main() {
    let ready = true;
    let a: u8 = 10;
    let value = if ready { 1 } else { a };
    println!("{value}");
    let text = if ready { "yes".to_string() } else { String::new() };
    println!("{text}");
}
1
yes

A Trailing Semicolon Changes the Value

Adding a semicolon after the final expression turns it into a statement, and the block then evaluates to () instead of the value. This is the most common mistake when using if as an expression.

// ❌ does not compile
fn main() {
    let ready = true;
    let value: i32 = if ready { 5; } else { 6; };
    println!("{value}");
}
error[E0308]: mismatched types
 --> main.rs:3:31
  |
3 |     let value: i32 = if ready { 5; } else { 6; };
  |                               ^^^-^^
  |                               |  |
  |                               |  help: remove this semicolon to return this value
  |                               expected `i32`, found `()`

error[E0308]: mismatched types
 --> main.rs:3:43
  |
3 |     let value: i32 = if ready { 5; } else { 6; };
  |                                           ^^^-^^
  |                                           |  |
  |                                           |  help: remove this semicolon to return this value
  |                                           expected `i32`, found `()`

The same applies to branches that end with a statement such as println!(...); followed by a value. Statements may come first, as long as the last line is the value:

fn main() {
    let n = 3;
    let v = if n > 2 {
        println!("big");
        10
    } else {
        println!("small");
        20
    };
    println!("{v}");

    let unit = if n > 2 { println!("a") } else { println!("b") };
    println!("{unit:?}");
}
big
10
a
()

println! returns (), so a branch that ends with it, with or without a semicolon, has type ().

else Is Required When the Value Is Used

Without an else, the if can only produce (). If the if block produces any other type, there is no value for the case where the condition is false, and the compiler rejects it.

// ❌ does not compile
fn main() {
    let n = 3;
    let label = if n > 5 { "large" };
    println!("{label}");
}
error[E0317]: `if` may be missing an `else` clause
 --> main.rs:3:17
  |
3 |     let label = if n > 5 { "large" };
  |                 ^^^^^^^^^^^-------^^
  |                 |          |
  |                 |          found here
  |                 expected `&str`, found `()`
  |
  = note: `if` expressions without `else` evaluate to `()`
  = help: consider adding an `else` block that evaluates to the expected type

The same rule applies to a chain of else if branches: the last branch must be a plain else.

if in Statement Position

When an if expression appears as a statement and its blocks produce a value other than (), the compiler expects () there and reports an error. Add a semicolon after the closing brace to discard the value:

// ❌ does not compile
fn main() {
    let c = true;
    if c { 1 } else { 2 }
    println!("hi");
}
error[E0308]: mismatched types
 --> main.rs:3:12
  |
3 |     if c { 1 } else { 2 }
  |     -------^-------------
  |     |      |
  |     |      expected `()`, found integer
  |     expected this to be `()`

error[E0308]: mismatched types
 --> main.rs:3:23
  |
3 |     if c { 1 } else { 2 }
  |     ------------------^--
  |     |                 |
  |     |                 expected `()`, found integer
  |     expected this to be `()`
fn main() {
    let c = true;
    if c { 1 } else { 2 };
    println!("hi");
}
hi

Forgetting the semicolon after a let that holds an if expression is a syntax error, because let is a statement that must end with ;:

// ❌ does not compile
fn main() {
    let ready = true;
    let value = if ready { 5 } else { 6 }
    println!("{value}");
}
error: expected `;`, found `println`
 --> main.rs:3:42
  |
3 |     let value = if ready { 5 } else { 6 }
  |                                          ^ help: add `;` here
4 |     println!("{value}");
  |     ------- unexpected token

if as a Function's Return Value

The last expression of a function body is its return value, so an if expression in tail position returns the selected branch. A semicolon after it discards the value and the function then returns ():

// ❌ does not compile
fn pick(c: bool) -> i32 {
    if c {
        1
    } else {
        2
    };
}

fn main() {
    println!("{}", pick(true));
}
error[E0308]: mismatched types
 --> main.rs:1:21
  |
1 | fn pick(c: bool) -> i32 {
  |    ----             ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
...
6 |     };
  |      - help: remove this semicolon to return this value

Only the Selected Branch Runs

The unselected branch is never evaluated. An expensive call, or any call with side effects, in the other branch does not run:

fn expensive(label: &str) -> i32 {
    println!("computing {label}");
    42
}

fn main() {
    let use_cache = true;
    let value = if use_cache { 7 } else { expensive("slow path") };
    println!("value = {value}");

    let use_cache = false;
    let value = if use_cache { 7 } else { expensive("slow path") };
    println!("value = {value}");
}
value = 7
computing slow path
value = 42

Branches That Never Produce a Value

A branch may end in an expression that does not complete normally: return, break, continue, or a call to panic!. These have the special type ! (the never type), which can be used where any type is expected. The other branch then decides the type of the if.

fn first_even(numbers: &[i32]) -> i32 {
    let mut result = -1;
    for &n in numbers {
        let candidate = if n % 2 == 0 { n } else { continue };
        result = candidate;
        break;
    }
    result
}

fn parse_or_exit(text: &str) -> i32 {
    let value = if let Ok(n) = text.parse::<i32>() {
        n
    } else {
        return -1;
    };
    value * 2
}

fn main() {
    println!("{}", first_even(&[1, 3, 8, 10]));
    println!("{}", first_even(&[1, 3]));
    println!("{}", parse_or_exit("21"));
    println!("{}", parse_or_exit("abc"));
}
8
-1
42
-1

In first_even, continue skips to the next element when n is odd, so candidate is only ever assigned an even number. In parse_or_exit, return -1; leaves the function, so value is only assigned when parsing succeeds.

A branch that panics works the same way:

fn checked_div(a: i32, b: i32) -> i32 {
    let result = if b != 0 {
        a / b
    } else {
        panic!("division by zero");
    };
    result
}

fn main() {
    println!("{}", checked_div(10, 2));
    println!("{}", checked_div(1, 0));
}
5
thread 'main' (374511) panicked at main.rs:5:9:
division by zero

The first call prints 5. The second call panics, and the program exits with status 101.

An if at the Start of a Statement

When if is the first thing in a statement, the parser treats it as a complete statement and does not continue the expression after the closing brace. An operator that follows is then a syntax error:

// ❌ does not compile
fn pick(c: bool) -> i32 {
    if c { 1 } else { 2 } + 10
}

fn main() {
    println!("{}", pick(true));
}
error: leading `+` is not supported
 --> main.rs:2:27
  |
2 |     if c { 1 } else { 2 } + 10
  |                           ^ unexpected `+`
  |
help: parentheses are required to parse this as an expression
  |
2 |     (if c { 1 } else { 2 }) + 10
  |     +                     +

Wrap the if in parentheses, or put it on the right side of the operator or in a let initializer, where it is already part of an expression:

fn main() {
    let ready = true;
    let value = (if ready { 1 } else { 2 }) + 10;
    let other = 10 + if ready { 1 } else { 2 };
    println!("{value} {other}");
}
11 11