Rust if let and while let
A match expression must cover every possible case. Sometimes only one case matters, and the rest deserve a shrug. The if let and while let forms give a lighter way to handle one pattern. Imagine checking a mailbox: you only care whether a letter exists, and an empty box needs no ceremony.
The Problem with Verbose match
The code below cares only about the Some case, yet it must still write an arm for None.
let favorite: Option<&str> = Some("mango");
match favorite {
Some(fruit) => println!("Favorite: {fruit}"),
None => {}
}Using if let
The if let form tests one pattern and runs the block when it matches.
if let Some(fruit) = favorite {
println!("Favorite: {fruit}");
}
favorite = Some("mango")
|
v
Does it fit Some(fruit)?
|yes |no
v v
run the block skip silently
Read the line as: if the value fits the pattern Some(fruit), bind the inner value to fruit and run the block.
Adding an else Branch
An else block handles every value that fails the pattern.
let score: Option<u32> = None;
if let Some(s) = score {
println!("Score is {s}");
} else {
println!("No score recorded");
}
No score recorded
Working with Enums
The pattern can target any enum variant, including those carrying data.
enum Message {
Quit,
Write(String),
Move { x: i32, y: i32 },
}
fn main() {
let msg = Message::Write(String::from("hello"));
if let Message::Write(text) = msg {
println!("Text: {text}");
}
}Variants that do not match are ignored. The compiler does not demand arms for Quit or Move.
Trade-Off Against match
The shorter form skips the safety net of exhaustive checking. Adding a new enum variant later triggers no compiler warning at an if let site. Use match when every variant deserves a decision, and use if let when one variant matters.
| Feature | match | if let |
|---|---|---|
| Covers all cases | Required | Optional |
| Code length | Longer | Shorter |
| Best for | Many variants | One variant |
The while let Loop
The while let loop repeats as long as a pattern keeps matching. A stack of plates shows the idea: keep removing the top plate until none remain.
fn main() {
let mut plates = vec![1, 2, 3];
while let Some(top) = plates.pop() {
println!("Removed plate {top}");
}
}
Removed plate 3 Removed plate 2 Removed plate 1
The pop method returns Some(value) while items remain and None once the vector is empty. The loop stops by itself at that moment.
pop() --> Some(3) --> body runs pop() --> Some(2) --> body runs pop() --> Some(1) --> body runs pop() --> None --> loop ends
let else for Early Exit
The let else form unpacks a pattern and leaves the function when the pattern fails. The happy path stays flat and readable.
fn describe(input: Option<u32>) {
let Some(value) = input else {
println!("Nothing to show");
return;
};
println!("Value: {value}");
}
fn main() {
describe(Some(8));
describe(None);
}
Value: 8 Nothing to show
The else block must leave the current flow with return, break, continue, or a panic.
Matching Results
The same syntax works for Result values when one outcome matters.
if let Ok(number) = "42".parse::<i32>() {
println!("Parsed {number}");
}Common Mistakes
Writing a comparison instead of a pattern is a frequent slip. The line if let Some(x) == value is invalid. The pattern sits on the left of a single equals sign, and the value sits on the right.
Using if let when every variant needs handling hides future bugs. A new enum variant compiles without a warning at an if let site. Switch to match in those places.
Creating an infinite while let loop happens when the expression never returns None. Verify that something inside the loop changes the data.
Practice Ideas
- Search a vector with
get(index)and print the item only when it exists. - Use
while letto drain a vector of names and print each one. - Rewrite a three-line
matchon anOptionas anif let.
Key Points
if lethandles a single pattern without writing every match arm.- An optional
elseblock covers all other values. while letloops until the pattern stops matching.let elseexits early when a pattern fails.- Use
matchwhen each variant needs a decision.
