Rustif let & while let

if let and while let in Rust

Rust's match expression is powerful — but sometimes you only care about one pattern and want to ignore everything else. Writing a full match just for that feels like overkill. That's where if let and while let come in: they give you the power of pattern matching with much less ceremony.

The Problem: Verbose match for a Single Pattern

Suppose you have an Option and only want to act when the value is Some. A full match looks like this:

RUST
let favourite_colour: Option<&str> = Some("blue");

match favourite_colour {
    Some(colour) => println!("Your favourite colour is {}", colour),
    None => (), // do nothing — just an empty arm to satisfy the compiler
}

The None => () arm exists purely to satisfy the compiler's exhaustiveness requirement. It adds noise without adding meaning. if let removes that noise entirely.

if let Syntax

if let combines a pattern and an expression into one construct. If the expression matches the pattern, the block runs and any bindings are available inside it. If it does not match, the block is skipped.

RUST
let favourite_colour: Option<&str> = Some("blue");

if let Some(colour) = favourite_colour {
    println!("Your favourite colour is {}", colour);
}

Read it as: "if favourite_colour matches the pattern Some(colour), run the block with colour bound to the inner value." No extra None arm needed.

Note
`if let` is syntax sugar over `match`. The compiler lowers it to a `match` with an ignored wildcard arm, so there is no runtime cost difference between the two.
if let with else

Attach an else block that runs when the pattern does not match — similar to the None arm in a match:

RUST
let config_max: Option<u8> = Some(3);

if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
} else {
    println!("No maximum configured, using default.");
}
Chaining with else if and else if let

if let chains naturally with else if and additional else if let arms, letting you handle several patterns or conditions in sequence without nesting:

RUST
let number: Option<u32> = Some(7);
let is_even = true;

if let Some(n) = number {
    println!("Got a number: {}", n);
} else if is_even {
    println!("No number, but the flag says even.");
} else {
    println!("No number and flag is false.");
}

Unlike match, the compiler does NOT check that if let chains cover all cases. That is a deliberate trade-off: you get brevity, but you lose the exhaustiveness guarantee.

Warning
Because `if let` chains are not exhaustive, you can silently miss cases that `match` would force you to handle. Prefer `match` when you need to be certain every variant is addressed.
if let with Enums (not just Option)

if let works with any enum, not only Option. Here is an example with a custom Coin enum that carries data inside one of its variants:

RUST
enum Coin {
    Penny,
    Quarter(String), // carries a state name
}

let coin = Coin::Quarter(String::from("Alaska"));

if let Coin::Quarter(state) = coin {
    println!("State quarter from {}!", state);
} else {
    println!("Not a quarter.");
}
while let: Loop While a Pattern Matches

while let is the looping counterpart to if let. The loop body runs as long as the expression keeps matching the pattern. The moment the match fails, the loop exits cleanly — no manual break needed.

The classic use-case is draining a stack:

RUST
let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

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

Vec::pop() returns Option<T>. When the vector is empty it returns None, the pattern fails to match, and the loop exits automatically. No manual length checks or break statements required.

Tip
`while let` shines wherever an operation signals "nothing left" by returning `None` — popping a stack, receiving from a channel, or advancing a custom iterator.
while let with Iterators

You can also use while let to manually drive an iterator. This is mostly equivalent to a for loop, but useful when you need extra control mid-loop (for example, skipping iter.next() on certain iterations):

RUST
let data = vec![10, 20, 30];
let mut iter = data.iter();

while let Some(value) = iter.next() {
    println!("value = {}", value);
}
let else (Rust 1.65+)

let else is the newest member of this family. It binds a pattern in a regular let statement and provides a diverging else block — one that must return, break, continue, or panic! — for when the pattern fails to match.

RUST
fn process(input: &str) -> u32 {
    let Ok(number) = input.trim().parse::<u32>() else {
        println!("Not a valid number, returning 0.");
        return 0;
    };

    // 'number' is bound here, in the surrounding scope — not locked inside a block
    number * 2
}

println!("{}", process("21"));   // 42
println!("{}", process("abc"));  // prints message, returns 0

The key difference from if let: the binding from let else lives in the surrounding scope, not inside a nested block. This avoids rightward indentation drift that accumulates with successive guard clauses.

Success
`let else` is the idiomatic guard-clause pattern in modern Rust: validate or unwrap at the top of a function, return early on failure, and continue with the clean bound value — all without extra nesting.
The matches! Macro

When you only need a bool result — no binding — the matches! macro is the most concise option:

RUST
let val: Option<i32> = Some(42);

// verbose — a full match just to produce a bool
let is_some = match val {
    Some(_) => true,
    None    => false,
};

// concise — matches! macro
let is_some = matches!(val, Some(_));

// also supports match guards
let is_positive = matches!(val, Some(n) if n > 0);

println!("{}", is_some);      // true
println!("{}", is_positive);  // true
Tip
Use `matches!` inside `filter`, `assert!`, and anywhere a boolean result from a pattern is all you need — no binding, no block, just true or false.
When to Use if let vs match

Scenario

Best tool

Handle one variant, ignore everything else

if let

Need exhaustive coverage of every variant

match

Loop until a pattern stops matching

while let

Early-return guard at the top of a function

let else

Boolean check with no binding needed

matches!

Multiple patterns, each with different logic

match

Practical Example: Parsing Optional Config Values

Here is a realistic snippet showing if let, while let, and let else working together to process optional command-line arguments:

RUST
fn apply_config(port: Option<&str>, retries: Option<&str>) {
    // let else: bail early if the port string is not a valid u16
    let port_str = port.unwrap_or("8080");
    let Ok(port_num) = port_str.parse::<u16>() else {
        eprintln!("Invalid port '{}', aborting.", port_str);
        return;
    };

    // if let: optional value — only act when present
    if let Some(r) = retries {
        if let Ok(n) = r.parse::<u8>() {
            println!("Retries set to {}", n);
        } else {
            println!("Invalid retry count '{}', ignoring.", r);
        }
    }

    println!("Server will start on port {}", port_num);
}

apply_config(Some("3000"), Some("5"));
apply_config(Some("bad"),  None);
Note
All the constructs on this page — `if let`, `while let`, `let else`, and `matches!` — are zero-cost abstractions. The compiler optimises them identically to hand-written `match` expressions. You pay nothing extra for the improved readability.