11.6if let, while let, and let else

Last updated June 13, 2026

match is thorough, and sometimes thoroughness is overkill. When you care about exactly one pattern and want to brush off everything else, a full match with a _ => {} arm is ceremony. Rust has three lighter constructs for these cases. Each is, under the hood, a match you didn't have to write out.

if let: one pattern, a side door

Suppose you want to do something only when an Option is Some, and nothing otherwise. The match version has a dead arm:

fn main() {
    let config: Option<i32> = Some(3);
    match config {
        Some(value) => println!("configured: {value}"),
        None => {}
    }
}

That None => {} is pure noise; it exists only to satisfy exhaustiveness. if let says the same thing without it:

fn main() {
    let config: Option<i32> = Some(3);
    if let Some(value) = config {
        println!("configured: {value}");
    }
}
configured: 3

Read it as "if config matches the pattern Some(value), run the block with value bound." If it doesn't match (it's None), the block is skipped. It's an if whose condition is a pattern match instead of a bool, and it binds the matched data just like a match arm. You can add an else for the non-matching case:

    if let Some(value) = config {
        println!("configured: {value}");
    } else {
        println!("using default");
    }

That else is the None arm, written as a normal else-block. if let ... else is the natural choice when you have a "present" case and a single "absent" case and don't need the full match apparatus.

Best practice

Use if let when you care about one pattern and can ignore the rest. Use match when you want the compiler to verify you've handled every case. The tradeoff is exactly that: if let is more concise but gives up exhaustiveness checking, so reach for it when "the other cases" genuinely don't need handling, not as a way to dodge cases you should handle.

while let: loop until the pattern fails

The same idea drives a loop. while let keeps looping as long as a pattern matches, and stops the moment it doesn't. The classic use pops items off a collection until it's empty:

fn main() {
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        println!("{top}");
    }
}
3
2
1

stack.pop() returns Option: Some(value) while there's something to remove, None when the stack is empty (this is Option earning its keep again, lesson 11.3). while let Some(top) = stack.pop() runs the body for each Some, binding the popped value to top, and exits cleanly when pop finally returns None. The alternative with loop and match and a break on None works too, but while let says the intent in one line. (vec! builds a growable list; chapter 18 is its proper home, but it's the natural thing to pop from here.)

let else: bind or bail

The third tool inverts if let. Often you want to extract a value and, if it isn't there, leave the current function entirely (return, break, continue, or panic). Nesting the rest of the function inside an if let block pushes everything to the right. let else keeps the happy path flat:

fn parse_age(input: &str) -> u32 {
    let Ok(age) = input.trim().parse::<u32>() else {
        println!("not a valid number");
        return 0;
    };
    age * 2
}

fn main() {
    println!("{}", parse_age("21"));     // 42
    println!("{}", parse_age("oops"));   // prints the message, then 0
}
42
not a valid number
0

let Ok(age) = ... else { ... }; says "bind age from the Ok, or else run this block, which must diverge (here, return)." The else block is required to leave the function (or loop), so that after the let else, age is guaranteed to exist and the rest of the function uses it without indentation. (Ok is Result's "success" variant, chapter 12's subject; it behaves just like Some for pattern purposes.)

let else is the antidote to the "arrow of doom," where each successive check nests one level deeper. Each unwrap-or-bail becomes a flat line, and the main logic stays at the left margin where it's readable.

Key insight

These three are all match with the boilerplate removed. if let is a match with one real arm and a _ you didn't type. while let is a loop plus that same match and an automatic break. let else is a match whose non-matching arm must diverge. Reach for them for readability; reach back for match whenever you want the compiler to hold you to every case.

Choosing between them

A quick guide. Want one case handled and the rest ignored: if let. Want one case plus a single fallback: if let ... else. Want to repeat while a pattern keeps matching: while let. Want to extract a value and exit early if it's missing: let else. Want every case checked by the compiler: match. They overlap, and more than one will often work; pick the one whose shape matches your intent most directly.

Quiz time

Question #1

Rewrite this match as an if let:

let result: Option<&str> = Some("found");
match result {
    Some(s) => println!("{s}"),
    None => {}
}
Show solution
let result: Option<&str> = Some("found");
if let Some(s) = result {
    println!("{s}");
}

The empty None arm disappears; if let simply does nothing when the pattern doesn't match.

Question #2

What does this print, and when does the loop stop?

fn main() {
    let mut nums = vec![10, 20, 30];
    while let Some(n) = nums.pop() {
        println!("{n}");
    }
    println!("done");
}
Show solution
30
20
10
done

pop removes from the end, returning Some each time until the vector is empty, at which point it returns None and while let exits. Then done prints.

Question #3

Why use let else instead of if let here? Rewrite the body using let else:

fn double_or_zero(maybe: Option<i32>) -> i32 {
    if let Some(n) = maybe {
        n * 2
    } else {
        0
    }
}
Show solution
fn double_or_zero(maybe: Option<i32>) -> i32 {
    let Some(n) = maybe else {
        return 0;
    };
    n * 2
}

let else keeps the main logic (n * 2) un-indented and handles the failure as an early return 0. For a one-liner this is a wash, but as the "happy path" grows, let else stays flat while nested if lets drift rightward. Either is correct here; let else is the choice when bailing out early reads more clearly than wrapping the rest in a block.

One thing left for enums: just as structs got methods in chapter 10, enums can have methods too. The next lesson puts impl on an enum and builds the classic example, a little state machine.