| name | rust-pattern-matching |
| description | > Use when this capability is needed. |
Rust Pattern Matching
When to Use
Invoke this skill before:
- Writing or reviewing any
match expression over a domain enum, Option, or Result.
- Diagnosing
error[E0004]: non-exhaustive patterns.
- Investigating a runtime panic from
unreachable!() or .unwrap() in a match arm.
- Introducing a new enum variant (each existing
_ => catch-all silently swallows it).
- Reviewing any nested
match/if let pyramid that could use let else or ?.
Core Idioms
let else for early-return on a missing value
let Some(user_id) = extract_user_id(&req) else {
return Err(AppError::Unauthenticated);
};
let user_id = match extract_user_id(&req) {
Some(id) => id,
None => return Err(AppError::Unauthenticated),
};
The Book (ch06-03): "Notice that it stays on the 'happy path' in the main body of the
function this way, without having significantly different control flow for two branches the
way the if let did."
if let only when the non-match arm is truly a no-op
if let Some(max) = config_max {
println!("max = {max}");
}
if let Some(x) = opt {
use_value(x);
} else {
use_default();
}
match opt {
Some(x) => use_value(x),
None => use_default(),
}
The Book (ch06-03): "Choosing between match and if let depends on what you're doing…
gaining conciseness is an appropriate trade-off for losing exhaustive checking."
Match guard instead of nested if inside an arm
match num {
Some(x) if x % 2 == 0 => println!("{x} is even"),
Some(x) => println!("{x} is odd"),
None => (),
}
match num {
Some(x) => {
if x % 2 == 0 { println!("{x} is even"); }
else { println!("{x} is odd"); }
}
None => (),
}
Guards bind more loosely than |: 4 | 5 | 6 if y means (4 | 5 | 6) if y.
Destructure in the pattern, not in the body
match msg {
Message::Move { x, y } => move_to(x, y),
Message::Write(text) => log(text),
Message::Quit => shutdown(),
Message::ChangeColor(r, g, b) => set_color(r, g, b),
}
match msg {
Message::Move(inner) => {
let x = inner.x;
let y = inner.y;
move_to(x, y);
}
}
@ binding to test and capture simultaneously
match msg {
Message::Hello { id: id @ 3..=7 } => println!("priority id {id}"),
Message::Hello { id: 10..=12 } => println!("secondary range"),
Message::Hello { id } => println!("other id {id}"),
}
match msg {
Message::Hello { id: 3..=7 } => println!("priority id (unknown value)"),
}
The Book (ch19-03): "@ lets us test a value and save it in a variable within one pattern."
Destructuring in for and function parameters
for (index, value) in v.iter().enumerate() { ... }
fn print_point(&(x, y): &(i32, i32)) { ... }
for pair in v.iter().enumerate() {
let (index, value) = pair;
}
Forbidden Patterns
Forbidden 1 — Catch-all _ => on a Domain Enum
Forbidden: A _ => wildcard (or a named catch-all other =>) as the only remaining arm
of a match over an application-defined enum.
Why (Book ch06-02): The compiler enforces exhaustiveness for explicitly listed arms. A
_ => arm absorbs every variant not listed above it, including variants added in the
future. Adding a new variant silently compiles; the new case falls through to _ => with
no warning and no forced review. The compiler's exhaustiveness guarantee — "Rust even knows
which pattern we forgot!" — is completely bypassed.
match status {
AgentStatus::Available => handle_available(),
AgentStatus::OnBreak => handle_break(),
_ => {}
}
match status {
AgentStatus::Available => handle_available(),
AgentStatus::OnBreak => handle_break(),
AgentStatus::Wrapup => handle_wrapup(),
AgentStatus::Offline => {}
}
Exception: _ => is acceptable when matching external types you do not control
(e.g., std::io::ErrorKind, third-party enums), or as the last arm of a match over an
integer/string literal set. Mark with // exhaustive: external type.
grep -rn '_ =>' src/ | grep -v '// exhaustive: external type' | grep -v '#\[cfg(test'
Forbidden 2 — match opt { Some(x) => x, None => panic!() } Instead of ? / let else
Forbidden: Manually destructuring Option or Result in a match or if let where
the non-matching case panics, calls unreachable!(), or silently returns a default.
Why (Book ch09-02, ch06-03): Rust provides the ? operator and let else exactly for
this. Manual panic arms bypass the error-propagation chain, lose context, and are invisible
to callers. .unwrap() in production paths is forbidden by the project rules; this is the
structural equivalent.
let conn = match pool.get() {
Ok(c) => c,
Err(e) => panic!("no connection: {e}"),
};
let value = match opt {
Some(v) => v,
None => unreachable!("should always be Some here"),
};
let conn = pool.get()?;
let Some(value) = opt else {
return Err(AppError::MissingValue);
};
grep -rn 'panic!\|unreachable!' src/ | grep -v '#\[cfg(test\|// @invariant'
Forbidden 3 — Nested match/if let Pyramids Instead of ? or let else
Forbidden: More than one level of match or if let nesting when the outer level only
exists to unwrap an Option/Result before passing to the inner logic.
Why (Book ch06-03): Nesting escalates indentation, inverts the happy path, and makes
the control flow opaque. ? or let else flattens the pyramid and keeps the happy path
linear.
async fn process(req: Request) -> Result<Response, AppError> {
if let Some(session) = req.session() {
if let Ok(user) = db.find_user(session.user_id).await {
if let Some(profile) = user.profile {
return Ok(build_response(profile));
}
}
}
Err(AppError::Unauthorized)
}
async fn process(req: Request) -> Result<Response, AppError> {
let session = req.session().ok_or(AppError::Unauthorized)?;
let user = db.find_user(session.user_id).await?;
let profile = user.profile.ok_or(AppError::Unauthorized)?;
Ok(build_response(profile))
}
grep -rn 'if let' src/ --include='*.rs' -A3 | grep 'if let'
Forbidden 4 — unreachable!() / _ => unreachable!() Masking Real Cases
Forbidden: Using unreachable!() as the body (or entire else branch) of a match arm
over a domain enum in production code, outside a #[cfg(test)] block.
Why (Book ch06-02, ch19-03): unreachable!() is a runtime panic. When a future
variant is added or when an assumption is violated, the process crashes instead of failing
gracefully or producing a compile error. It also suppresses the exhaustiveness signal — the
compiler sees the arm and considers the match exhaustive; you lose the forced-review
property you'd get from a missing arm.
match event {
CallEvent::Started => handle_start(),
CallEvent::Ended => handle_end(),
_ => unreachable!("unexpected call event"),
}
match event {
CallEvent::Started => handle_start(),
CallEvent::Ended => handle_end(),
CallEvent::Held => handle_hold(),
CallEvent::Resumed => handle_resume(),
}
grep -rn 'unreachable!' src/ | grep -v '#\[cfg(test\|// @invariant'
Forbidden 5 — Cargo-Culted &Some(ref x) Binding (Pre-2018 Style)
Forbidden: Writing &Some(ref x), &Ok(ref e), or manually sprinkling ref inside
patterns when matching a reference.
Why (Rust Reference): Rust's default binding modes (Edition 2018+) automatically
dereference the scrutinee and bind variables by reference when matching a &T. There is no
need to write ref or & inside patterns in ordinary match expressions. The cargo-culted
form is unnecessary noise in Edition 2018+ — binding modes handle the reference
automatically, and the explicit form confuses readers familiar with modern Rust.
match &some_option {
&Some(ref x) => use_ref(x),
&None => {},
}
match &some_option {
Some(x) => use_ref(x),
None => {},
}
match some_option {
Some(x) => consume(x),
None => {},
}
grep -rn '\bref\b' src/ --include='*.rs' | grep -v '//' | grep -v '#\[cfg(test'
Forbidden 6 — Ignoring Result::Err via _ or Silently Swallowing It
Forbidden: Matching Result with a _ => arm or Ok(_) | Err(_) => () construction
that discards the error path without logging or propagating.
Why (Book ch06-02): The exhaustiveness guarantee exists to prevent exactly this. A
Result::Err that falls into _ is a silent failure — the operation failed but the caller
receives no signal. Clippy's unused_must_use lint catches the top-level case but not a
match that explicitly handles every arm (even if the Err arm does nothing).
match db.save(record).await {
Ok(row) => process(row),
_ => {}
}
if let Ok(row) = db.save(record).await {
process(row);
}
let row = db.save(record).await?;
process(row);
match db.save(record).await {
Ok(row) => process(row),
Err(err) => tracing::error!(?err, "save failed; skipping record"),
}
grep -rn -E 'Err\(_\)\s*=>\s*\{\}|Err\(_\)\s*=>\s*\(\)' src/ --include='*.rs' | grep -v '//'
grep -rn -E '_ =>\s*\{\}|_ =>\s*\(\)' src/ --include='*.rs' -B3 | grep -E 'match.*Result|\.await'
Refutability Quick Reference
| Construct | Accepts | Compiler reaction on wrong kind |
|---|
let x = … | Irrefutable only | E0005 — refutable pattern in local binding |
fn f(x: T) | Irrefutable only | same |
for x in … | Irrefutable only | same |
if let / while let | Both (refutable preferred) | warn(irrefutable_let_patterns) — Rust emits a lint warning, no W-code |
let … else | Refutable (irrefutable makes else dead code) | warning |
match last arm | Irrefutable (covers remainder) | E0004 if total is not exhaustive |
match non-last arms | Refutable | — |
The Book (ch19-02): "Function parameters, let statements, and for loops can only accept
irrefutable patterns, because the program cannot do anything meaningful when values don't
match."
Binding Modes (Default Ref Bindings)
When you match a &T value, Rust Edition 2018+ automatically adjusts the binding mode so
that variables in the pattern bind by reference. You do not need ref or & decorators
inside the pattern.
let points: Vec<Point> = vec![Point { x: 1, y: 2 }];
for Point { x, y } in &points {
println!("{x}, {y}");
}
Only write ref when you explicitly need a reference inside a pattern where the scrutinee
is owned (rare, usually in complex nested destructuring).
Book References
All citations from The Rust Programming Language (2024 edition):
| Topic | Chapter | Canonical URL |
|---|
Enums and match introduction | Ch 6.0 | https://doc.rust-lang.org/book/ch06-00-enums.html |
match exhaustiveness, E0004, wildcards, binding | Ch 6.2 | https://doc.rust-lang.org/book/ch06-02-match.html |
if let, let else | Ch 6.3 | https://doc.rust-lang.org/book/ch06-03-if-let.html |
| All the places patterns can be used | Ch 19.1 | https://doc.rust-lang.org/book/ch19-01-all-the-places-for-patterns.html |
| Refutability | Ch 19.2 | https://doc.rust-lang.org/book/ch19-02-refutability.html |
Pattern syntax (or-patterns, ranges, @, guards, destructuring, _, ..) | Ch 19.3 | https://doc.rust-lang.org/book/ch19-03-pattern-syntax.html |
Note on chapter numbering: The 2024 edition renumbered the Patterns chapter from
Ch 18 to Ch 19. The original URLs (ch18-*) redirect to the ch19-* equivalents.
Verification Hooks
Run all six detectors in a single sweep to audit a codebase for pattern-matching violations:
echo "=== F1: catch-all _ => on domain enums ==="
grep -rn '_ =>' src/ --include='*.rs' | grep -v '// exhaustive: external type' | grep -v '#\[cfg(test'
echo "=== F2: manual panic/unreachable in match arms ==="
grep -rn 'panic!\|unreachable!' src/ --include='*.rs' | grep -v '#\[cfg(test\|// @invariant'
echo "=== F3: nested if-let (heuristic — fires on ALL if-let; inspect for nesting) ==="
grep -rn 'if let' src/ --include='*.rs' -A3 | grep 'if let'
echo "=== F4: unreachable! outside test context ==="
grep -rn 'unreachable!' src/ --include='*.rs' | grep -v '#\[cfg(test\|// @invariant'
echo "=== F5: cargo-culted ref inside patterns ==="
grep -rn '\bref\b' src/ --include='*.rs' | grep -v '//' | grep -v '#\[cfg(test'
echo "=== F6a: explicit Err(_) arm silently discarded ==="
grep -rn -E 'Err\(_\)\s*=>\s*\{\}|Err\(_\)\s*=>\s*\(\)' src/ --include='*.rs' | grep -v '//'
echo "=== F6b: wildcard arm on Result-producing expression ==="
grep -rn -E '_ =>\s*\{\}|_ =>\s*\(\)' src/ --include='*.rs' -B3 | grep -E 'match.*Result|\.await'
Related Skills
rust-panic-vs-result — when to use ? vs explicit match vs unwrap; the full taxonomy
of panic-vs-propagate decisions.
rust-traits-generics — when pattern matching interacts with impl Trait or generic
bounds (e.g., matching on a Box<dyn Error> requires downcast_ref, not patterns).
rust-error-handling — AppError enum design, #[from] chains, and how exhaustive
match on AppError in IntoResponse is the correct mapping layer.
Source: adelabdelgawad/rust-fullstack-agents — distributed by TomeVault.