blog.dopana

Back

While match is powerful and ensures exhaustiveness, sometimes you only care about one specific pattern and want to ignore everything else. In those cases, writing a full match expression with a boilerplate _ => () arm introduces unnecessary clutter.

Rust provides if let syntax to make your code more concise and readable.

Explain Like I’m 10: The Mystery Gift Box#

Imagine you have a mystery gift box:

  • Inside, there might be a toy (Some(toy)), or it might be completely empty (None).
  • You only care if there is a toy inside so you can start playing with it immediately; if it’s empty, you simply walk away without doing anything.
graph TD
    Input["Input Value (Option / Enum)"] --> Check{"Matches Target Pattern?"}
    Check -- Yes --> Bind["Extract Value & Execute if let block"]
    Check -- No --> ElseCheck{"Is there an else block?"}
    ElseCheck -- Yes --> ElseAction["Execute else block"]
    ElseCheck -- No --> Ignore["Ignore and continue"]

Comparing match vs if let#

Suppose we only want to print a configuration value if it is Some(u8):

Using match (Verbose)#

src/main.rs
let config_max = Some(3u8);

match config_max {
    Some(max) => println!("The maximum is configured to be {max}"),
    _ => (), // Required boilerplate to satisfy exhaustiveness
}
rust

Using if let (Concise)#

src/main.rs
let config_max = Some(3u8);

if let Some(max) = config_max {
    println!("The maximum is configured to be {max}");
}
rust

[!TIP] Think of if let as syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values.

Combining if let with else#

We can also include an else block (which acts exactly like the _ => ... arm in a match expression):

Comparing: if let vs Standard if#

Rust beginners often ask: Why do we need if let when we already have standard if statements?

The key distinction lies in pattern matching and destructuring:

src/main.rs
let score = Some(95);

// 1. Using standard `if`: checks only a Boolean expression (true/false)
if score.is_some() {
    // Requires manual unwrapping to access the inner value -> Error-prone & verbose
    let val = score.unwrap();
    println!("Score: {val}");
}

// 2. Using `if let`: checks pattern AND safely binds inner values in one go
if let Some(val) = score {
    println!("Score: {val}"); // 100% type-safe, no .unwrap() needed
}
rust
graph TD
    subgraph IfOrdinary ["Standard if"]
        Cond["Evaluates Boolean condition (true/false)"] --> Unsafe["Manual unwrap required to access inner data"]
    end
    subgraph IfLet ["if let"]
        Pattern["Matches pattern"] --> Safe["Automatically destructures data & binds local variables safely"]
    end

When to Use if, if let, or match?#

CriteriaStandard ifif letmatch
Condition typeBoolean expression (bool)Pattern to matchAll possible patterns
Data destructuringNo (requires manual unwrapping)Yes (auto binds inner values)Yes (binds per matching arm)
Exhaustiveness checkNoNo (handles 1 pattern)Yes (enforces 100% coverage)
Branching1 (if) or 2 (if/else)1 (if let) or 2 (if let/else)Any number of arms
Best use caseNumeric comparisons, boolean flagsExtracting data from Option/Enum for a single variantComplex state machines, multi-pattern flows

[!WARNING] Choosing if let means losing the compiler’s exhaustive type checking that match gives you. Consider whether your logic could accidentally overlook newly added enum variants.

Summary#

  • if let allows you to concisely handle values that match a single pattern while ignoring the rest.
  • An optional else block handles the non-matching cases.
  • Use if let to reduce boilerplate when full pattern matching with match is unnecessarily verbose.

References#