Pattern matching
Pattern matching is one of Rust’s most distinctive features. The match expression admits exhaustive discrimination on any pattern; if let, while let, and let else admit single-pattern matching; function parameters and let bindings admit destructuring patterns. The pattern grammar is rich: literals, wildcards, ranges, struct/enum destructuring, tuple patterns, slice patterns, reference patterns, guards, and combinations of these. The combination — exhaustive match, single-arm if let, binding let, function-parameter patterns — admits substantial dispatching power throughout the language.
This page covers the pattern grammar, the contexts where patterns appear, and the conventional patterns. The relationship to enums (the conventional sum types) is in Data structures.
The pattern grammar
A pattern is matched against a value; if the match succeeds, any variables in the pattern are bound to the corresponding parts of the value. The principal pattern forms:
| Pattern | Example | Matches |
|---|---|---|
| Literal | 0, 'a', "hello", true | The exact value |
| Variable | x, name | Anything; binds to the variable |
| Wildcard | _ | Anything; binds nothing |
| Or | 1 | 2 | 3 | Any of the alternatives |
| Range | 1..=10, 'a'..='z' | A value in the range |
| Tuple | (a, b), (1, 2) | Tuples of matching shape |
| Array/slice | [1, 2, 3], [head, .., tail] | Sequences of matching shape |
| Struct | Point { x, y }, Point { x: 0, .. } | Structs of matching kind |
| Enum variant | Some(x), Color::Red | Enum variants |
| Reference | &x, &mut x | Reference patterns |
| Binding | name @ pattern | Match the pattern AND bind the whole |
| Guard | pat if cond | Match plus a boolean condition |
Where patterns appear
Patterns are used in several contexts:
// match expression:
match value {
Pattern1 => arm1,
Pattern2 => arm2,
}
// if let:
if let Pattern = value {
// ...
}
// while let:
while let Pattern = value {
// ...
}
// let else (since 1.65):
let Pattern = value else { return; };
// Function parameters:
fn process((x, y): (i32, i32)) { /* ... */ }
// for loop:
for (i, x) in v.iter().enumerate() {
// ...
}
// let bindings:
let (a, b) = (1, 2);
let Point { x, y } = p;
The patterns work consistently across these contexts; mastering the pattern grammar admits its use throughout.
Literal patterns
The simplest pattern: equality with a constant:
match n {
0 => "zero",
1 => "one",
-1 => "minus one",
_ => "other",
}
Literal patterns admit numbers, characters, strings (since 1.83 for &str), true, false. Constants from const declarations also work:
const SPECIAL: i32 = 42;
match n {
SPECIAL => "the answer",
_ => "other",
}
Variable patterns
A simple identifier captures the matched value:
match value {
x => println!("got {}", x), // captures value as x
}
A variable pattern alone matches anything; conventionally used inside more elaborate patterns:
match point {
(x, y) => println!("({}, {})", x, y),
}
Within a match, the variable pattern shadows any outer variable of the same name:
let n = 5;
match 10 {
n => println!("got {}", n), // n binds to 10; shadows outer n
}
println!("{}", n); // outer n; still 5
The shadowing is occasionally surprising; for matching against a known value, use a constant or compare in a guard.
Or-patterns
Match any of several alternatives:
match c {
'a' | 'e' | 'i' | 'o' | 'u' => "vowel",
_ => "consonant",
}
match status {
Status::Active | Status::Pending => "running",
Status::Stopped | Status::Failed => "stopped",
}
Captures inside or-patterns must bind the same names across alternatives:
match value {
Move(x, y) | Resize(x, y) => process(x, y),
}
Range patterns
Match a range of values:
match c {
'0'..='9' => "digit",
'a'..='z' => "lowercase letter",
'A'..='Z' => "uppercase letter",
_ => "other",
}
match age {
0..=12 => "child",
13..=19 => "teenager",
20..=64 => "adult",
_ => "senior",
}
The ..= admits inclusive ranges; this is the conventional form in patterns. The .. (exclusive) range is rarely used in patterns.
Tuple patterns
Destructure tuples:
let (a, b, c) = (1, 2, 3);
match point {
(0, 0) => "origin",
(_, 0) => "x-axis",
(0, _) => "y-axis",
(x, y) if x == y => "diagonal",
_ => "other",
}
The _ admits skipping a position; the variable pattern admits binding.
Array and slice patterns
Match against arrays and slices:
let arr = [1, 2, 3, 4, 5];
match arr {
[1, _, _, _, _] => "starts with 1",
[_, _, _, _, 5] => "ends with 5",
[first, .., last] => format!("first {} last {}", first, last),
_ => "other",
}
let v = vec![1, 2, 3];
match v.as_slice() {
[] => "empty",
[x] => format!("single: {}", x),
[x, y] => format!("pair: {}, {}", x, y),
[x, .., y] => format!("starts with {}, ends with {}", x, y),
}
The .. admits matching any number of elements (zero or more). The head, .. and .., tail forms are conventional.
The [head, rest @ ..] form binds rest to the remaining slice:
match v.as_slice() {
[first, rest @ ..] => {
println!("first: {}", first);
println!("rest: {:?}", rest);
}
[] => println!("empty"),
}
Struct patterns
Destructure structs:
struct Point { x: f64, y: f64 }
let p = Point { x: 3.0, y: 4.0 };
match p {
Point { x: 0.0, y: 0.0 } => "origin",
Point { x: 0.0, y } => format!("y-axis at {}", y),
Point { x, y: 0.0 } => format!("x-axis at {}", x),
Point { x, y } => format!("({}, {})", x, y),
}
The shorthand Point { x, y } binds the fields by their declared names; the Point { x: foo } form admits renaming.
The .. admits ignoring remaining fields:
match p {
Point { x: 0.0, .. } => "on y-axis",
Point { x, .. } => format!("x = {}", x),
}
Enum patterns
Destructure enum variants:
enum Shape {
Circle(f64),
Rectangle { width: f64, height: f64 },
Square(f64),
}
match shape {
Shape::Circle(r) => format!("circle r={}", r),
Shape::Rectangle { width, height } => format!("{}x{}", width, height),
Shape::Square(side) => format!("square {}", side),
}
The pattern matches the variant and destructures its content. For tuple-like variants (Circle(f64)), the position is matched; for struct-like variants (Rectangle { width, height }), the fields are matched.
The conventional standard-library enums:
match opt {
Some(value) => process(value),
None => use_default(),
}
match result {
Ok(value) => process(value),
Err(e) => log_error(e),
}
Reference patterns
Match through a reference:
let v = vec![1, 2, 3];
for x in &v { // x is &i32
match x {
&0 => println!("zero"), // dereference pattern
&n => println!("{}", n), // also dereference
}
}
// Or with the modern auto-deref:
for x in &v {
match x {
0 => println!("zero"), // implicit dereference
n => println!("{}", n),
}
}
The &pattern admits matching through a reference; the modern match ergonomics (since 2018) admits omitting the explicit & in many cases.
Binding patterns (@)
The @ admits matching a pattern AND binding the whole:
let n = 5;
match n {
x @ 1..=5 => println!("small {}", x),
x @ 6..=10 => println!("medium {}", x),
x => println!("other {}", x),
}
The x @ 1..=5 matches a value in the range and binds it to x. The conventional uses are when both the structure and the value matter.
Guards
A pattern may carry an if-clause:
match n {
x if x > 0 => "positive",
0 => "zero",
_ => "negative",
}
match (x, y) {
(a, b) if a == b => "equal",
(a, b) if a > b => "first larger",
_ => "second larger or equal",
}
The guard is evaluated after the pattern matches; if false, the case is rejected. The mechanism admits combining structural matching with arbitrary boolean conditions.
Exhaustiveness
match is exhaustive — every possible value of the discriminant must be covered:
enum Color { Red, Green, Blue }
let c = Color::Red;
match c {
Color::Red => "red",
Color::Green => "green",
// ERROR: non-exhaustive; Blue not covered
}
The conventional defences:
- Cover every variant — preferred for closed enums.
- Add a default arm (
_ => "...") — for open sets or when only some variants matter.
The exhaustiveness checking catches newly added variants; adding Color::Yellow produces an error in every match that doesn’t handle it. The mechanism is one of Rust’s most distinctive safety features.
For non-exhaustive matching where only specific variants matter, if let is the conventional alternative:
if let Color::Red = c {
println!("it's red");
}
if let and while let
if let admits one-arm pattern matching:
let opt = Some(42);
if let Some(n) = opt {
println!("got {}", n);
}
while let admits looping while the pattern matches:
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
Both treated in Conditionals and Loops.
let else
Since Rust 1.65:
fn parse_age(s: &str) -> Option<i32> {
let Some(n) = s.parse::<i32>().ok() else {
return None;
};
if !(0..=150).contains(&n) {
return None;
}
Some(n)
}
The let pattern = expr else { ... } admits “bind the matched value, or run the else block (which must diverge — return, break, continue, panic, etc.)”.
The conventional Rust pattern for “bind or early return”.
Patterns in function parameters
fn distance((x, y): (f64, f64), (a, b): (f64, f64)) -> f64 {
((x - a).powi(2) + (y - b).powi(2)).sqrt()
}
fn process(Point { x, y }: Point) -> f64 {
x * y
}
The function parameter is itself a pattern; the patterns admit destructuring at the point of binding.
The conventional Rust style uses parameter patterns sparingly; the explicit let (x, y) = pair; inside the body is often clearer.
Patterns in let bindings
let (a, b, c) = (1, 2, 3);
let Point { x, y } = origin;
let [first, second, _] = [10, 20, 30];
let Some(n) = compute() else { // let-else
return;
};
The destructuring let is conventional in Rust for unpacking tuples and structs.
Complex patterns
Pattern combinations admit substantial discrimination:
enum Event {
Click { x: i32, y: i32, button: Button },
Key { code: u32 },
Scroll(i32),
}
fn handle(event: &Event) {
match event {
Event::Click { x, y, button: Button::Left } if *x > 0 && *y > 0 => {
click_in_quadrant(*x, *y);
}
Event::Click { button: Button::Right, .. } => {
show_menu();
}
Event::Key { code: 27 } => { // escape
close_window();
}
Event::Scroll(delta) if *delta > 0 => {
scroll_up(*delta);
}
_ => {}
}
}
The combination — enum variants, struct fields, literals, guards — admits substantial discrimination in one expression.
Common patterns
Sum-type dispatch
enum Result<T, E> {
Ok(T),
Err(E),
}
match result {
Ok(v) => process(v),
Err(e) => log(e),
}
The conventional pattern for handling both success and failure.
Type-and-value matching
match parse(input) {
Ok(n) if n > 0 => println!("positive: {}", n),
Ok(n) if n < 0 => println!("negative: {}", n),
Ok(_) => println!("zero"),
Err(e) => println!("error: {}", e),
}
The pattern admits structure and value-level discrimination.
Destructuring at the binding site
fn distance((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
The destructuring in the parameter list admits direct field access.
Slice head and tail
fn process(items: &[i32]) -> Result<(), String> {
match items {
[] => Err("no items".to_string()),
[single] => process_single(*single),
[first, second, rest @ ..] => {
process_pair(*first, *second);
process(rest)
}
}
}
The pattern admits recursive list processing in the Haskell style.
State machine via match
enum State {
Idle,
Running { start: Instant },
Done { result: i32 },
Failed { error: String },
}
match state {
State::Idle => start(),
State::Running { start } if start.elapsed() > Duration::from_secs(60) => timeout(),
State::Running { .. } => continue_running(),
State::Done { result } => return *result,
State::Failed { error } => panic!("{}", error),
}
The combination admits expressing state-machine transitions clearly.
Error propagation
let value = match parse(input) {
Ok(v) => v,
Err(e) => return Err(e),
};
// Or, with the let-else form:
let Ok(value) = parse(input) else {
return Err("parse failed".to_string());
};
// Or, with the ? operator (most idiomatic):
let value = parse(input)?;
The ? operator is the conventional Rust form for error propagation; treated in Error handling.
A note on the conventional discipline
The contemporary Rust pattern-matching advice:
- Use
matchfor value-driven discrimination on enums. - Use
if letwhen only one variant matters. - Use
let elsefor “bind or early return”. - Use exhaustive matching — let the compiler verify every case.
- Use guards sparingly — usually a separate
ifis clearer. - Use destructuring
letfor unpacking tuples and structs.
The combination — pattern grammar, exhaustive match, if let for single-arm matching, let else for early return — is the substance of Rust’s pattern matching. The mechanism is more capable than C-family switch and admits substantial declarative discrimination in idiomatic code.