Concise Control Flow with if let in Rust
Master if let and if let else in Rust: write cleaner, less verbose code when matching a single pattern without match boilerplate.
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)#
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (), // Required boilerplate to satisfy exhaustiveness
}rustUsing if let (Concise)#
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}rust[!TIP] Think of
if letas syntax sugar for amatchthat 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):
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
California,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn count_coin(coin: Coin, count: &mut i32) {
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
*count += 1;
}
}
fn main() {
let mut count = 0;
let coin1 = Coin::Quarter(UsState::Alaska);
let coin2 = Coin::Penny;
count_coin(coin1, &mut count);
count_coin(coin2, &mut count);
println!("Count of non-quarter coins: {count}");
}rustComparing: 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:
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
}rustgraph 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?#
| Criteria | Standard if | if let | match |
|---|---|---|---|
| Condition type | Boolean expression (bool) | Pattern to match | All possible patterns |
| Data destructuring | No (requires manual unwrapping) | Yes (auto binds inner values) | Yes (binds per matching arm) |
| Exhaustiveness check | No | No (handles 1 pattern) | Yes (enforces 100% coverage) |
| Branching | 1 (if) or 2 (if/else) | 1 (if let) or 2 (if let/else) | Any number of arms |
| Best use case | Numeric comparisons, boolean flags | Extracting data from Option/Enum for a single variant | Complex state machines, multi-pattern flows |
[!WARNING] Choosing
if letmeans losing the compiler’s exhaustive type checking thatmatchgives you. Consider whether your logic could accidentally overlook newly added enum variants.
Summary#
if letallows you to concisely handle values that match a single pattern while ignoring the rest.- An optional
elseblock handles the non-matching cases. - Use
if letto reduce boilerplate when full pattern matching withmatchis unnecessarily verbose.