RustPattern Matching

Pattern Matching in Rust

Patterns are a special syntax in Rust for matching against the structure of types. A pattern describes a shape — and when a value fits that shape, the match succeeds and you can optionally bind pieces of the value to names. Patterns appear in match arms, if let, while let, for loops, let statements, and function parameters. Together they form one of the most expressive and safe parts of the language.

Where Patterns Are Used

Patterns are not limited to match — they appear in several places across Rust.

  • match arms — the primary home of patterns; every arm is a pattern

  • if let — a concise alternative when you only care about one variant

  • while let — loop as long as a pattern keeps matching

  • for loops — destructure each item of an iterator in place

  • let statements — every let x = ... is a pattern (x binds the whole value)

  • Function parameters — destructure arguments at the call site

RUST
// match arm
match some_value {
    Some(x) => println!("got {}", x),
    None    => println!("nothing"),
}

// if let — fires only when the pattern matches
if let Some(x) = some_value {
    println!("got {}", x);
}

// while let — loop until the pattern stops matching
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("{}", top);
}

// for loop — destructure each tuple element
let pairs = vec![(0, 'a'), (1, 'b'), (2, 'c')];
for (index, letter) in &pairs {
    println!("{}: {}", index, letter);
}

// let statement — the whole left side is a pattern
let (x, y, z) = (1, 2, 3);

// function parameter destructuring
fn print_pair(&(first, second): &(i32, i32)) {
    println!("({}, {})", first, second);
}
Literal Patterns

The simplest pattern is an exact literal value. The pattern matches only if the scrutinee equals that value precisely. Literals can be integers, characters, booleans, or string slices.

RUST
fn classify(n: i32) -> &'static str {
    match n {
        0  => "zero",
        1  => "one",
        2  => "two",
        -1 => "negative one",
        _  => "something else",
    }
}

fn describe_bool(b: bool) -> &'static str {
    match b {
        true  => "yes",
        false => "no",
    }
}

fn vowel_check(c: char) -> &'static str {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' => "lowercase vowel",
        'A' | 'E' | 'I' | 'O' | 'U' => "uppercase vowel",
        _                             => "consonant or other",
    }
}

fn main() {
    println!("{}", classify(0));             // zero
    println!("{}", classify(2));             // two
    println!("{}", classify(99));            // something else
    println!("{}", describe_bool(true));     // yes
    println!("{}", vowel_check('e'));        // lowercase vowel
}
Variable (Binding) Patterns

A plain identifier in a pattern position is a variable pattern. It matches anything and binds the matched value to that name. This is what every plain let x = ... statement uses — x is a variable pattern that matches any value.

RUST
fn main() {
    let number = 42;

    match number {
        // 'n' is a variable pattern — binds whatever 'number' is
        n => println!("The number is {}", n),
    }

    // Variable patterns in let statements
    let x = 10;           // x binds 10
    let (a, b) = (1, 2);  // a binds 1, b binds 2

    println!("x={} a={} b={}", x, a, b);

    // The difference between a variable pattern and wildcard:
    let value = String::from("hello");
    match value {
        // 's' takes ownership — value is moved into s
        s => println!("got: {}", s),
        // '_' would NOT move ownership
    }
}
Note
A variable pattern always succeeds — it never rejects a value. A bare name at the end of a `match` acts as a catch-all just like `_`, but it also binds the value so you can use it in the arm's body.
The Wildcard Pattern _

The underscore _ is the wildcard pattern. It matches any value but does not bind it to a name, and crucially it does not take ownership. Use it when you want to discard a value intentionally.

RUST
fn main() {
    let pair = (1, 99);

    match pair {
        (0, _) => println!("first is zero, second ignored"),
        (_, 0) => println!("second is zero, first ignored"),
        (x, _) => println!("first is {}, second ignored", x),
    }
    // first is 1, second ignored

    // Ignore an entire value in a for loop
    for _ in 0..3 {
        println!("tick");
    }
    // tick (x3)

    // Silence unused-variable warnings during development
    let _in_progress = setup_something();
}

fn setup_something() -> i32 { 0 }
Tip
`_` and `_name` are subtly different. A bare `_` never binds the value — ownership is not transferred, so the original value can still be used afterward. A `_name` binding does transfer ownership (or copy) and suppresses the unused-variable warning, but the value cannot be used after the pattern.
Range Patterns

Use the inclusive range pattern ..= to match any value within a span. Range patterns work with integer and character literals. Exclusive .. ranges are not supported inside patterns — only the inclusive ..= form is allowed.

RUST
fn grade(score: u32) -> &'static str {
    match score {
        90..=100 => "A",
        80..=89  => "B",
        70..=79  => "C",
        60..=69  => "D",
        0..=59   => "F",
        _        => "invalid score",
    }
}

fn char_kind(c: char) -> &'static str {
    match c {
        'a'..='z' => "lowercase letter",
        'A'..='Z' => "uppercase letter",
        '0'..='9' => "digit",
        _         => "other character",
    }
}

fn main() {
    println!("{}", grade(97));        // A
    println!("{}", grade(73));        // C
    println!("{}", grade(40));        // F
    println!("{}", char_kind('m'));   // lowercase letter
    println!("{}", char_kind('7'));   // digit
    println!("{}", char_kind('!'));   // other character
}
Multiple Patterns with |

The pipe | separates multiple patterns in a single arm. The arm fires if the scrutinee matches any one of them. You can mix literals, ranges, and | freely within one arm.

RUST
fn main() {
    let day: u8 = 6; // 1=Mon … 7=Sun

    let kind = match day {
        1 | 2 | 3 | 4 | 5 => "weekday",
        6 | 7              => "weekend",
        _                  => "invalid day",
    };
    println!("{}", kind); // weekend

    // Mix ranges and literals in the same arm
    let n: i32 = 15;
    match n {
        0         => println!("zero"),
        1 | 2 | 3 => println!("one, two, or three"),
        4..=10    => println!("four through ten"),
        11..=20   => println!("eleven through twenty"),
        _         => println!("out of handled range"),
    }
    // eleven through twenty

    // Useful for HTTP status codes
    let status = 403u16;
    match status {
        200 | 201 | 204      => println!("success"),
        301 | 302            => println!("redirect"),
        400 | 401 | 403 | 404 => println!("client error"),
        500..=599            => println!("server error"),
        _                    => println!("other"),
    }
    // client error
}
Tuple Patterns

Tuples can be matched element by element. You can mix literals, variable bindings, and wildcards within a tuple pattern to match only the structure you care about.

RUST
fn classify_point(p: (i32, i32)) -> &'static str {
    match p {
        (0, 0) => "origin",
        (x, 0) => "on the x-axis",  // 0 must match second element; x is bound
        (0, y) => "on the y-axis",  // 0 must match first element; y is bound
        _      => "somewhere in the plane",
    }
}

fn describe_rgb(color: (u8, u8, u8)) -> &'static str {
    match color {
        (255, 0,   0  ) => "pure red",
        (0,   255, 0  ) => "pure green",
        (0,   0,   255) => "pure blue",
        (r,   g,   b  ) if r == g && g == b => "shade of grey",
        _               => "mixed color",
    }
}

fn main() {
    println!("{}", classify_point((0, 0)));   // origin
    println!("{}", classify_point((5, 0)));   // on the x-axis
    println!("{}", classify_point((0, 3)));   // on the y-axis
    println!("{}", classify_point((2, 7)));   // somewhere in the plane

    println!("{}", describe_rgb((255, 0, 0)));     // pure red
    println!("{}", describe_rgb((128, 128, 128))); // shade of grey
    println!("{}", describe_rgb((10, 20, 30)));    // mixed color
}
Struct Patterns

Struct patterns destructure a struct, binding its fields to variable names. You can match specific field values while binding others, and use .. to skip fields you do not need. Field shorthand works: Point { x, y } binds field x to the name x and field y to the name y.

RUST
#[derive(Debug)]
struct Point { x: i32, y: i32 }

fn describe_point(p: &Point) -> String {
    match p {
        Point { x: 0, y: 0 } => String::from("at the origin"),
        Point { x: 0, y }    => format!("on the y-axis at y={}", y),
        Point { x, y: 0 }    => format!("on the x-axis at x={}", x),
        Point { x, y }       => format!("at ({}, {})", x, y),
    }
}

#[derive(Debug)]
struct Rectangle {
    top_left:     Point,
    bottom_right: Point,
    label:        String,
}

fn describe_rect(r: &Rectangle) {
    // Nested struct destructuring; ignore 'label' with ..
    let Rectangle {
        top_left:     Point { x: x1, y: y1 },
        bottom_right: Point { x: x2, y: y2 },
        ..
    } = r;
    println!("rect from ({},{}) to ({},{})", x1, y1, x2, y2);
}

fn main() {
    println!("{}", describe_point(&Point { x: 0, y: 5 }));  // on the y-axis at y=5
    println!("{}", describe_point(&Point { x: 3, y: 0 }));  // on the x-axis at x=3
    println!("{}", describe_point(&Point { x: 2, y: 4 }));  // at (2, 4)

    let r = Rectangle {
        top_left:     Point { x: 1, y: 10 },
        bottom_right: Point { x: 8, y: 2  },
        label:        String::from("main rect"),
    };
    describe_rect(&r); // rect from (1,10) to (8,2)
}
Enum Patterns

Enum patterns are where Rust's pattern matching truly shines. Each variant is a distinct pattern. Tuple-style variants destructure positionally; struct-style variants destructure by field name; unit variants are matched by name alone.

RUST
#[derive(Debug)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

fn process(msg: Message) {
    match msg {
        Message::Quit =>
            println!("Quit message received"),

        Message::Move { x, y } =>
            println!("Move to ({}, {})", x, y),

        Message::Write(text) =>
            println!("Write: {}", text),

        Message::ChangeColor(r, g, b) =>
            println!("Color: rgb({}, {}, {})", r, g, b),
    }
}

fn main() {
    process(Message::Quit);
    process(Message::Move { x: 10, y: 20 });
    process(Message::Write(String::from("hello")));
    process(Message::ChangeColor(255, 128, 0));
}
// Quit message received
// Move to (10, 20)
// Write: hello
// Color: rgb(255, 128, 0)

RUST
// Option and Result are enums — same pattern syntax applies
fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    match safe_divide(10.0, 2.0) {
        Some(result) => println!("result: {}", result), // result: 5
        None         => println!("division by zero"),
    }

    // Nested enum patterns: Option<Result<...>>
    let nested: Option<Result<i32, &str>> = Some(Ok(42));
    match nested {
        Some(Ok(n))  => println!("success: {}", n),      // success: 42
        Some(Err(e)) => println!("inner error: {}", e),
        None         => println!("no value"),
    }
}
Match Guards

A match guard adds an extra if condition after a pattern. The arm fires only if the pattern matches AND the guard condition is true. Guards let you express conditions that patterns alone cannot capture.

RUST
fn main() {
    let pair = (4, -4);

    match pair {
        (x, y) if x == y      => println!("equal"),
        (x, y) if x + y == 0  => println!("opposite signs: {} and {}", x, y),
        (x, _) if x % 2 == 0  => println!("first is even: {}", x),
        (x, y)                 => println!("other: ({}, {})", x, y),
    }
    // opposite signs: 4 and -4

    // Guards combine naturally with enum patterns
    let numbers = vec![Some(5), None, Some(-3), Some(100), None];
    for opt in &numbers {
        match opt {
            Some(n) if *n > 0 && *n < 50 => println!("{} is small positive", n),
            Some(n) if *n >= 50           => println!("{} is large positive", n),
            Some(n)                       => println!("{} is non-positive", n),
            None                          => println!("no value"),
        }
    }
    // 5 is small positive
    // no value
    // -3 is non-positive
    // 100 is large positive
    // no value
}
Warning
Match guards do not affect exhaustiveness checking. The compiler still requires that the patterns collectively cover all cases, ignoring guards. A guard can only narrow which arm fires — it cannot satisfy exhaustiveness on its own.
@ Bindings — Capture and Test at Once

The @ operator lets you simultaneously test a value against a pattern AND bind it to a name. Without @, you must choose: either test a range (and lose the actual value) or bind with a variable (and lose the test). The @ operator gives you both.

RUST
fn describe_number(n: i32) -> String {
    match n {
        // Bind to 'small' AND verify it falls in 1..=5
        small @ 1..=5  => format!("{} is a small positive number", small),
        // Bind to 'mid' AND verify it falls in 6..=20
        mid   @ 6..=20 => format!("{} is a medium number", mid),
        other          => format!("{} is outside the handled range", other),
    }
}

fn main() {
    println!("{}", describe_number(3));   // 3 is a small positive number
    println!("{}", describe_number(15));  // 15 is a medium number
    println!("{}", describe_number(99));  // 99 is outside the handled range

    // @ inside a struct pattern
    struct Sensor { id: u32, reading: f64 }

    let s = Sensor { id: 7, reading: 98.6 };
    match s {
        Sensor { id: n @ 1..=10, reading } =>
            println!("sensor {} reading: {:.1}", n, reading),
        Sensor { id, .. } =>
            println!("unknown sensor {}", id),
    }
    // sensor 7 reading: 98.6

    // @ with Option patterns
    let msg: Option<u32> = Some(42);
    match msg {
        Some(n @ 1..=100) => println!("in range: {}", n),   // in range: 42
        Some(n)           => println!("out of range: {}", n),
        None              => println!("nothing"),
    }
}
Nested Patterns

Patterns can be nested to arbitrary depth, letting you match the structure of complex, layered data in a single expression without unpacking manually.

RUST
#[derive(Debug)]
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),
    }
}

fn main() {
    // (2 + 3) * 4
    let expr = Expr::Mul(
        Box::new(Expr::Add(
            Box::new(Expr::Num(2.0)),
            Box::new(Expr::Num(3.0)),
        )),
        Box::new(Expr::Num(4.0)),
    );
    println!("{}", eval(&expr)); // 20

    // Nested Option matching
    let nested: Option<Option<i32>> = Some(Some(7));
    match nested {
        Some(Some(n)) if n > 0 => println!("positive inner value: {}", n),
        Some(Some(n))          => println!("non-positive inner value: {}", n),
        Some(None)             => println!("outer Some wraps None"),
        None                   => println!("outer None"),
    }
    // positive inner value: 7
}
Irrefutable vs Refutable Patterns

Rust distinguishes two categories of patterns:

  • Irrefutable — always match, regardless of the value (e.g. x, (a, b)). Required in let statements, function parameters, and for loops.
  • Refutable — might not match (e.g. Some(x) does not match None). Used in if let, while let, and match arms.

Placing a refutable pattern where an irrefutable one is required is a compile error.

RUST
fn main() {
    // Irrefutable: (x, y) always matches any 2-tuple — OK in let
    let (x, y) = (1, 2);
    println!("{} {}", x, y);

    // ERROR — refutable pattern in let statement:
    // let Some(n) = Some(5); // compiler: "pattern is refutable"

    // CORRECT: use if let for refutable patterns
    let opt: Option<i32> = Some(10);
    if let Some(n) = opt {
        println!("got {}", n); // got 10
    }

    // while let: keep looping while the pattern matches
    let mut optional = Some(0u32);
    while let Some(n) = optional {
        println!("n = {}", n);
        optional = if n < 3 { Some(n + 1) } else { None };
    }
    // n = 0
    // n = 1
    // n = 2
    // n = 3
}
.. to Ignore Remaining Fields or Elements

Use .. in a struct or tuple pattern to ignore the fields or elements you do not need. This is especially useful for large structs — it prevents your patterns from breaking every time a new field is added.

RUST
struct Config {
    host:    String,
    port:    u16,
    timeout: u32,
    retries: u8,
    debug:   bool,
}

fn connect(cfg: &Config) {
    // Only care about host and port; ignore everything else with ..
    let Config { host, port, .. } = cfg;
    println!("Connecting to {}:{}", host, port);
}

fn main() {
    let cfg = Config {
        host:    String::from("localhost"),
        port:    8080,
        timeout: 30,
        retries: 3,
        debug:   false,
    };
    connect(&cfg); // Connecting to localhost:8080

    // .. in a tuple pattern — ignore middle elements
    let numbers = (1, 2, 3, 4, 5);
    let (first, .., last) = numbers;
    println!("first={} last={}", first, last); // first=1 last=5

    // .. in a tuple struct
    struct Rgb(u8, u8, u8);
    let color = Rgb(255, 128, 0);
    let Rgb(r, ..) = color;
    println!("red channel: {}", r); // red channel: 255
}
Pattern Matching Quick Reference

Pattern type

Syntax example

What it does

Literal

42, true, 'a'

Matches exactly that value

Variable

x, name

Matches anything and binds to the name

Wildcard

_

Matches anything; no binding, no ownership transfer

Range

1..=5, 'a'..='z'

Matches values within the inclusive range

Multiple

1 | 2 | 3

Matches any of the listed patterns

Tuple

(x, 0, y)

Matches tuple structure element by element

Struct

Point { x, y: 0 }

Destructures struct fields by name

Enum variant

Some(x), Err(e)

Matches a specific enum variant and its data

Guard

n if n > 0

Adds an extra boolean condition after the pattern

@ binding

n @ 1..=10

Tests the pattern and binds the value simultaneously

Ignore rest

Point { x, .. }

Skips unspecified fields or tuple elements

Success
You now have a comprehensive understanding of Rust's pattern matching system. Patterns appear throughout the language — in match arms, if let, while let, for loops, let statements, and function parameters. They can be nested arbitrarily deep to match complex data. Match guards add extra conditions, @ bindings capture and test in one step, and .. silences the need to enumerate every field. Mastering patterns is one of the most impactful steps in becoming fluent in Rust.