Pattern Matching
Rust pattern matching with match, if let, while let, destructuring, and advanced match patterns
You are an expert in Rust pattern matching for writing expressive, exhaustive, and safe control flow using match expressions and destructuring.
## Key Points
- Prefer `match` over chains of `if/else if` when checking multiple variants of an enum — the compiler ensures exhaustiveness.
- Use `if let` when you only need to handle one variant and want to ignore the rest.
- Use `let-else` for early returns on pattern failure — it is cleaner than `if let` with an else block that returns.
- Use `matches!` for boolean checks against patterns, especially in filter closures.
- Avoid the wildcard `_` catch-all if you can enumerate all variants — you will get a compile error if a new variant is added, which is a feature.
- Destructure directly in function parameters when the structure is simple:
- **Non-exhaustive match**: Forgetting a variant causes a compile error — this is intentional and one of Rust's safety guarantees. Use `_` only when you truly want to ignore remaining cases.
- **Shadowing in patterns**: A variable name in a pattern creates a new binding, it does not compare against an existing variable. Use a guard for comparison:
- **Forgetting to handle `None` and `Err`**: The compiler forces you to handle all cases, but using `_` to discard errors silently is a logic bug.
- **Move in match arms**: Matching on an owned value moves it. If you need the value after matching, match on a reference (`match &value`).
- **Guard expressions are not part of exhaustiveness**: The compiler does not consider guards when checking exhaustiveness, so you still need a catch-all arm.
## Quick Example
```rust
fn is_vowel(c: char) -> bool {
matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U')
}
```
```rust
let status = 404;
let is_client_error = matches!(status, 400..=499);
let option: Option<i32> = Some(42);
let has_value = matches!(option, Some(x) if x > 0);
```skilldb get rust-skills/Pattern MatchingFull skill: 267 linesPattern Matching — Rust Programming
You are an expert in Rust pattern matching for writing expressive, exhaustive, and safe control flow using match expressions and destructuring.
Core Philosophy
Pattern matching in Rust is more than syntactic sugar for switch statements -- it is the primary mechanism for safely decomposing data. When you match on an enum, the compiler proves that every variant is handled. When you destructure a struct, you name exactly the fields you care about. This exhaustiveness guarantee transforms runtime "forgot a case" bugs into compile-time errors.
The design philosophy is that data and control flow should be tightly coupled. An enum defines the shape of data; a match expression defines the behavior for each shape. Adding a new variant to an enum immediately surfaces every match that needs updating, across the entire codebase. This is why Rust programmers prefer enums over boolean flags, stringly-typed tags, or inheritance hierarchies -- the compiler enforces completeness.
Pattern matching also encourages a functional decomposition style. Rather than mutating state through method chains, you destructure values, bind the pieces you need, and produce new values. Combined with let-else for early returns and matches! for boolean checks, patterns let you write control flow that reads declaratively while remaining rigorously checked.
Anti-Patterns
- Wildcard catch-all on enums you control: Using
_ =>as the last arm means adding a new variant compiles silently instead of flagging every match that needs attention. Enumerate all variants explicitly so the compiler catches missing cases. - Deeply nested
if letchains instead ofmatch: Writingif let Some(x) = a { if let Ok(y) = b { ... } }obscures control flow and loses exhaustiveness checking. Flatten into a singlematchon a tuple of the values, or uselet-elsefor early returns. - Shadowing variables accidentally in patterns: A bare name in a pattern creates a new binding -- it does not compare against an existing variable with that name. This compiles without warning and silently matches everything. Use match guards (
v if v == expected) for comparisons. - Using
matches!when you need the inner value:matches!returns a boolean and discards the destructured data. If you need the bound value, useif letormatchinstead of callingmatches!and then extracting the value separately. - Ignoring
ErrandNonevariants in match arms: WritingErr(_) => {}orNone => {}with an empty body silently swallows failures. If the case genuinely requires no action, add a comment explaining why; otherwise, handle or propagate the error.
Overview
Pattern matching is one of Rust's most powerful features. The match expression provides exhaustive, compiler-checked branching. Patterns appear in match, if let, while let, let bindings, function parameters, and for loops. The compiler ensures every possible case is handled, preventing missed edge cases at compile time.
Core Concepts
Basic match Expression
enum Direction {
North,
South,
East,
West,
}
fn describe(dir: Direction) -> &'static str {
match dir {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
}
}
Matching with Values
fn classify_age(age: u32) -> &'static str {
match age {
0 => "newborn",
1..=12 => "child",
13..=19 => "teenager",
20..=64 => "adult",
65.. => "senior",
}
}
Destructuring Enums
enum Command {
Quit,
Echo(String),
Move { x: i32, y: i32 },
Color(u8, u8, u8),
}
fn execute(cmd: Command) {
match cmd {
Command::Quit => println!("quitting"),
Command::Echo(msg) => println!("{msg}"),
Command::Move { x, y } => println!("moving to ({x}, {y})"),
Command::Color(r, g, b) => println!("color: #{r:02x}{g:02x}{b:02x}"),
}
}
Destructuring Structs and Tuples
struct Point {
x: f64,
y: f64,
}
let point = Point { x: 3.0, y: 4.0 };
let Point { x, y } = point;
println!("x={x}, y={y}");
// Tuple destructuring
let (first, second, _) = (1, 2, 3);
if let and while let
// if let: when you only care about one variant
let config_max: Option<u32> = Some(100);
if let Some(max) = config_max {
println!("max is {max}");
}
// while let: loop while pattern matches
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}");
}
let-else (Rust 1.65+)
fn process(value: Option<String>) {
let Some(name) = value else {
println!("no value provided");
return;
};
// name is bound here as String
println!("processing {name}");
}
Implementation Patterns
Guards
fn classify(value: i32) -> &'static str {
match value {
n if n < 0 => "negative",
0 => "zero",
n if n % 2 == 0 => "positive even",
_ => "positive odd",
}
}
Binding with @
fn check_age(age: u32) {
match age {
a @ 0..=17 => println!("minor, age {a}"),
a @ 18..=65 => println!("working age, {a}"),
a => println!("retired, age {a}"),
}
}
Or Patterns
fn is_vowel(c: char) -> bool {
matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U')
}
Nested Destructuring
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
fn eval(expr: &Expr) -> f64 {
match expr {
Expr::Num(n) => *n,
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Mul(a, b) => eval(a) * eval(b),
}
}
Matching References
let values = vec![1, 2, 3, 4, 5];
for value in &values {
match value {
&1 => println!("one"),
&2 => println!("two"),
other => println!("{other}"),
}
}
// Or more commonly with dereferencing
for &value in &values {
match value {
1 => println!("one"),
2 => println!("two"),
other => println!("{other}"),
}
}
The matches! Macro
let status = 404;
let is_client_error = matches!(status, 400..=499);
let option: Option<i32> = Some(42);
let has_value = matches!(option, Some(x) if x > 0);
Matching on Result and Option Chains
fn process_input(input: &str) -> Result<(), String> {
match input.parse::<i32>() {
Ok(n) if n > 0 => println!("positive: {n}"),
Ok(0) => println!("zero"),
Ok(n) => println!("negative: {n}"),
Err(e) => return Err(format!("parse error: {e}")),
}
Ok(())
}
Best Practices
- Prefer
matchover chains ofif/else ifwhen checking multiple variants of an enum — the compiler ensures exhaustiveness. - Use
if letwhen you only need to handle one variant and want to ignore the rest. - Use
let-elsefor early returns on pattern failure — it is cleaner thanif letwith an else block that returns. - Use
matches!for boolean checks against patterns, especially in filter closures. - Avoid the wildcard
_catch-all if you can enumerate all variants — you will get a compile error if a new variant is added, which is a feature. - Destructure directly in function parameters when the structure is simple:
fn distance(&(x1, y1): &(f64, f64), &(x2, y2): &(f64, f64)) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
Common Pitfalls
- Non-exhaustive match: Forgetting a variant causes a compile error — this is intentional and one of Rust's safety guarantees. Use
_only when you truly want to ignore remaining cases. - Shadowing in patterns: A variable name in a pattern creates a new binding, it does not compare against an existing variable. Use a guard for comparison:
let expected = 42;
match value {
expected => {} // This binds `value` to `expected`, it does NOT compare!
}
// Instead:
match value {
v if v == expected => {}
_ => {}
}
- Forgetting to handle
NoneandErr: The compiler forces you to handle all cases, but using_to discard errors silently is a logic bug. - Move in match arms: Matching on an owned value moves it. If you need the value after matching, match on a reference (
match &value). - Guard expressions are not part of exhaustiveness: The compiler does not consider guards when checking exhaustiveness, so you still need a catch-all arm.
Install this skill directly: skilldb add rust-skills
Related Skills
Async Rust
Async Rust programming with async/await, Tokio runtime, futures, and concurrent task patterns
Cargo Workspace
Cargo workspaces, project structure, dependency management, and multi-crate Rust project organization
Error Handling
Rust error handling with Result, Option, the ? operator, and ecosystem crates anyhow and thiserror
Lifetimes
Rust lifetime annotations for ensuring reference validity and understanding the borrow checker
Ownership Borrowing
Rust ownership, borrowing, and move semantics for writing memory-safe code without a garbage collector
Smart Pointers
Rust smart pointers Box, Rc, Arc, RefCell, and their combinations for heap allocation and shared ownership