rust-syntax-pattern-matching
The mechanics of pattern matching in Rust: match, if let, let else, while let, if-let chains (edition 2024, stable in 1.88), destructuring of tuples / structs / enums / slices / references, ref and ref mut, or-patterns, guards, @ bindings, .. rest patterns, range patterns, and literal patterns.
Cross-references: [[rust-syntax-edition-2024]] (if-let chain edition-gating, if let temporary scope change) [[rust-syntax-ownership]] (move vs borrow in match arms, when ref matters)
When to use this skill
- User writes a
matchand gets E0004 ("non-exhaustive patterns") or wants to know which arms are missing - User asks "should I use
match,if let, orlet elsehere" - User wonders why their
if letchain is rejected (edition or version too old) - User destructures a tuple, struct, enum, slice, or reference and the compiler complains about moves
- User asks what
ref/ref mutdoes and when to reach for it - User wants or-patterns (
A | B), guards (if cond), or@bindings explained - User asks about
..rest patterns in structs or slices - User writes a range pattern (
1..=10) or asks about exclusive ranges in patterns (stable in 1.80) - User gets E0408 (variable not bound in all patterns) or E0409 (variable bound with different mode/type)
For broader edition 2024 changes (RPIT lifetime capture, unsafe extern, never type fallback) see [[rust-syntax-edition-2024]]. For move vs borrow semantics that drive ref see [[rust-syntax-ownership]].
Quick reference table
| Construct | Refutable? | Use when | Example |
|---|---|---|---|
let |
irrefutable only | bind a value that cannot fail | let (a, b) = pair; |
let else (1.65) |
refutable, else MUST diverge | extract one variant, bail otherwise | let Some(x) = opt else { return; }; |
if let |
refutable | one variant matters, ignore the rest | if let Some(x) = opt { ... } |
if let ... else |
refutable | two branches, one bound | if let Some(x) = opt { ... } else { ... } |
if let chain (1.88, ed 2024) |
refutable | combine multiple let + boolean conditions |
if let A = x && y > 0 && let B = z { ... } |
while let |
refutable | loop while pattern matches | while let Some(item) = iter.next() { ... } |
match |
exhaustive over scrutinee | branch on every variant of an enum / value | match x { Some(n) => ..., None => ... } |
ALWAYS prefer match when every variant must be handled. ALWAYS prefer if let / let else when only one variant matters.
Decision tree: which construct to use
Need every variant handled (exhaustive)?
├── YES → use `match`
└── NO → only one variant matters?
├── failure must short-circuit (return / panic / break)?
│ └── YES → use `let else`
├── two simple branches (matched / not matched)?
│ └── YES → use `if let { ... } else { ... }`
├── multiple conditions chained with &&?
│ └── YES → use `if let` chain (edition 2024, Rust 1.88+)
└── loop on consecutive matches?
└── YES → use `while let`
match : exhaustiveness, arm order, wildcards
enum Shape { Circle(f64), Square(f64), Rect(f64, f64) }
fn area(s: Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Square(s) => s * s,
Shape::Rect(w, h) => w * h,
}
}
Rules :
- Exhaustiveness : every possible value of the scrutinee MUST be covered. Missing arms produce E0004 ("non-exhaustive patterns").
- Arm order : first matching arm wins. Place more specific arms before more general ones.
- Wildcard
_: the catch-all pattern. Does not bind. - Bind-all
name:name => ...binds the value toname. Use when the value is still useful. #[non_exhaustive]enums from external crates ALWAYS require a_arm. The compiler refuses to assume the enum is closed.- Empty types (Rust 1.82+) :
Result<T, Infallible>can omit theErrarm becauseInfalliblehas no constructors.
ALWAYS write _ => unreachable!("...") instead of _ => () when you believe the arm is impossible, this surfaces logic bugs at runtime instead of silently swallowing them.
Source: Rust Book Ch 6.2, The match Control Flow Construct Source: Rust Reference, match expressions
if let : single pattern, optional else
if let Some(value) = optional {
use_value(value);
}
if let Ok(parsed) = "42".parse::<i32>() {
println!("{parsed}");
} else {
println!("not a number");
}
Rules :
if let PAT = expr { ... }runs the block only whenexprmatchesPAT. Bindings fromPATare in scope inside the block.elsebranch is optional. When present, the binding is NOT in scope insideelse(the match failed, so it could not be bound).- Edition 2024 change : temporaries in the
if letscrutinee are dropped at the end of theif letarm, not at the end of the containing scope. This fixes drop-order bugs but can break pre-2024 code that depended on the old behaviour. See [[rust-syntax-edition-2024]].
Source: Rust Book Ch 6.3, Concise Control Flow with if let Source: edition 2024 if-let scope
if let chains : edition 2024, Rust 1.88+
Stabilized in Rust 1.88.0 (2025-06-26). Only available on edition 2024 because the feature depends on the if let temporary scope change.
// 2024 edition only, Rust 1.88+
if let Some(user) = lookup(id)
&& user.is_active
&& let Some(email) = user.email
{
send(email);
}
Rules :
- ANY combination of
let PAT = exprand boolean expressions, joined by&&. - ALL conditions must evaluate to true (and all
let-patterns must match) for the body to run. - Bindings from earlier
let-patterns are in scope in later conditions and in the body. ||is NOT allowed acrossletconditions, only&&.- NEVER use this on edition 2021 or earlier, it will not compile. Migrate the crate or use nested
if let.
ALWAYS prefer an if let chain over deep nesting when targeting edition 2024.
Source: Rust 1.88.0 release notes, let chains stabilization
let else : extract one variant or diverge
Stabilized in Rust 1.65 (Nov 2022).
fn parse_config(s: &str) -> Config {
let Ok(cfg) = serde_json::from_str(s) else {
return Config::default();
};
cfg
}
Rules :
- The
elsebranch MUST diverge :return,break,continue,panic!,unreachable!,std::process::exit, or any expression of type!. - Falling through the
elseblock is a hard compile error. - Bindings from the pattern are in scope after the statement, in the surrounding block, unlike
if let, which scopes them inside the block only. - Use
let elseto flatten the happy path. Replaceslet x = match opt { Some(x) => x, None => return };.
NEVER write let Some(x) = opt else { /* nothing */ };, the compiler rejects it. The else must diverge.
Source: Rust 1.65 release notes, let-else Source: Rust Reference, let statements
while let : loop while pattern matches
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}");
}
Rules :
- The loop runs the body each iteration as long as the pattern matches.
- The loop stops on the first non-match.
- The binding is in scope inside the loop body only.
- NEVER use
while letfor an iterator whenforworks,for x in iterdesugars to aloop+matchand handlesIterator::nextreturningNonecorrectly without the visual overhead.
USE while let when the source is not a regular iterator (e.g. VecDeque::pop_front, Receiver::recv returning a Result, repeated next_if).
Source: Rust Book Ch 6.3, while let
Destructuring
Patterns can take apart compound values in let, match, function parameters, and closures.
Tuples and tuple structs
let (x, y, z) = (1, 2, 3);
struct Point(i32, i32);
let Point(a, b) = Point(3, 4);
Structs (named fields)
struct User { name: String, age: u32 }
let u = User { name: "Sam".into(), age: 42 };
let User { name, age } = u; // shorthand: same field names
let User { name: n, age: a } = u; // rebind to local names
let User { name, .. } = u; // ignore remaining fields with `..`
Rule : if a struct has more fields than you destructure and you OMIT .., the compiler emits E0027 ("pattern does not mention field"). ALWAYS use .. when you intentionally ignore fields.
Enums
match result {
Ok(value) => use_value(value),
Err(e) => log_error(e),
}
Slices and arrays
match slice {
[] => println!("empty"),
[only] => println!("one: {only}"),
[first, .., last] => println!("{first} ... {last}"),
[first, rest @ ..] => println!("first={first}, rest={rest:?}"),
}
Rule : .. matches any number of elements (zero or more). Combined with @ it binds the rest as a sub-slice.
References
let value = 5;
let r = &value;
let &v = r; // destructure to get owned i32 (Copy required)
Source: Rust Book Ch 18, Patterns and Matching Source: Rust Reference, Patterns
ref and ref mut : bind by reference
By default, a binding pattern moves or copies the matched value. ref makes the binding a reference instead. ref mut makes it a mutable reference.
let s = String::from("hi");
match s {
ref borrowed => println!("{borrowed}"),
}
// s is still usable here, no move
Modern style : ALWAYS prefer to take a reference at the scrutinee instead of using ref in arm patterns. Match ergonomics (RFC 2005) handles the rest.
// Idiomatic
match &s {
text => println!("{text}"),
}
Use ref only when you cannot take a reference at the scrutinee, typically inside an if let chain or destructuring a value you cannot reshape.
WARNING : Edition 2024 restricts some match-ergonomics combinations with & patterns to keep room for future syntax. If you hit a confusing error on a complex destructure, see [[rust-syntax-edition-2024]].
Source: Rust Reference, identifier patterns
Or-patterns, guards, @ bindings, .. rest, ranges, literals
Or-patterns
match n {
1 | 2 | 3 => "small",
_ => "other",
}
Rule (E0408 / E0409) : every alternative MUST bind the same variables with the same types. Or-patterns with different bindings are rejected.
Guards
match pair {
(x, y) if x == y => "equal",
(x, y) if x > y => "first bigger",
_ => "other",
}
Rule : the compiler does NOT consider guard expressions for exhaustiveness. A guarded arm is treated as potentially failing. ALWAYS provide a fallback arm.
@ bindings
match age {
n @ 0..=12 => println!("child {n}"),
n @ 13..=19 => println!("teen {n}"),
n => println!("adult {n}"),
}
name @ pattern binds name to the entire value while also testing it against pattern.
.. rest
struct Big { a: u8, b: u8, c: u8, d: u8 }
let Big { a, .. } = big;
In structs : .. ignores all unmentioned fields. In tuples and slices : .. ignores any number of elements.
Range patterns
match digit {
'0'..='9' => "digit",
'a'..='z' => "lower",
_ => "other",
}
// Exclusive ranges (Rust 1.80+), pattern matches 0,1,2,...,9 (NOT 10)
match n {
0..10 => "single digit",
_ => "two or more digits",
}
ALWAYS use ..= for inclusive ranges. .. is exclusive (1.80+ in patterns).
Literal patterns
match cmd.as_str() {
"quit" => exit(0),
"help" => print_help(),
other => unknown(other),
}
Strings (&str), characters, integers, floats (deprecated in patterns), and bool can be literal patterns. ALWAYS bind the wildcard so you can include the value in error output.
Source: Rust Reference, Patterns Source: Rust 1.80 release notes, exclusive ranges in patterns
Common pitfalls
For the full list (with code examples and fixes) see references/anti-patterns.md. Highlights :
- Missing
_arm on#[non_exhaustive]enums from external crates, compile error let elsebody that does not diverge, compile error- Using nested
if let { if let { ... } }on edition 2024 instead of a chain, readability regression match value { ref x => ... }instead ofmatch &value { x => ... }, works but reads awkwardly- Or-pattern arms that bind variables of different types (
Some(n) | Ok(n)wherenhas two unrelated types), E0409 - Omitting
..when destructuring a struct with extra fields, E0027 - Relying on guard exhaustiveness, the compiler does not see through guards
- Forgetting that
if letchains need edition 2024 AND Rust 1.88+
Further reading
references/methods.md: full list of pattern syntax and constructs with one-line definitionsreferences/examples.md: copy-pasteable end-to-end examples (state machine, parser dispatch, slice patterns, complex destructure)references/anti-patterns.md: the eight pitfalls in full, with compiler error text and fix
Sources
Last verified : 2026-05-19.