Polyglot
Languages Rust pattern matching
Rust § pattern-matching

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:

PatternExampleMatches
Literal0, 'a', "hello", trueThe exact value
Variablex, nameAnything; binds to the variable
Wildcard_Anything; binds nothing
Or1 | 2 | 3Any of the alternatives
Range1..=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
StructPoint { x, y }, Point { x: 0, .. }Structs of matching kind
Enum variantSome(x), Color::RedEnum variants
Reference&x, &mut xReference patterns
Bindingname @ patternMatch the pattern AND bind the whole
Guardpat if condMatch 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 match for value-driven discrimination on enums.
  • Use if let when only one variant matters.
  • Use let else for “bind or early return”.
  • Use exhaustive matching — let the compiler verify every case.
  • Use guards sparingly — usually a separate if is clearer.
  • Use destructuring let for 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.