---
title: "11.6 if let, while let, and let else"
description: "Three lighter alternatives to match for when you only care about one pattern: if let for a single case, while let for looping, and let else for early exit."
url: "https://learnrust.net/chapter-11/if-let-while-let-let-else/"
last_updated: "2026-06-13"
---

# 11.6 if let, while let, and let else

`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:

```rust
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:

```rust
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:

```rust
    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:

```rust
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](https://learnrust.net/chapter-11/option/)). `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:

```rust
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`:

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

<details class="solution">
<summary>Show solution</summary>

```rust
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.

</details>

**Question #2**

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

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

<details class="solution">
<summary>Show solution</summary>

```
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.

</details>

**Question #3**

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

```rust
fn double_or_zero(maybe: Option<i32>) -> i32 {
    if let Some(n) = maybe {
        n * 2
    } else {
        0
    }
}
```

<details class="solution">
<summary>Show solution</summary>

```rust
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 let`s 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.

</details>

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.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
