Skip to main content
Rust · lesson 13 of 30
Rust beginner Lesson 13 of 30

Pattern Matching in Rust

Master Rust pattern matching: match, ranges, destructuring tuples, structs and nested enums, guards, @ bindings, if let, while let, slice patterns.

By SkillByExample · Updated

match Expressions

match is one of Rust’s most powerful features. It compares a value against a series of patterns and executes the first matching arm — and crucially, it is an expression that produces a value. Unlike switch in C or Java, match in Rust is exhaustive: the compiler rejects any match that does not cover every possible value. This means you can never accidentally forget a case.

fn main() {
    for number in [1, 3, 7, 10, 42] {
        let description = match number {
            1       => "one",
            2 | 3   => "two or three",   // | matches either pattern
            4..=9   => "between four and nine", // inclusive range pattern
            10      => "ten",
            _       => "something else", // _ is the catch-all (required to be exhaustive)
        };

        println!("{} is {}", number, description);
    }
}
$ rustc pattern_matching_match_expressions.rs && ./pattern_matching_match_expressions
1 is one
3 is two or three
7 is between four and nine
10 is ten
42 is something else

Rules:

  • Arms are checked top-to-bottom; the first match wins.
  • match must be exhaustive — all possible values must be covered.
  • _ is the wildcard that matches anything (like default in other languages).
  • Each arm is pattern => expression. Multi-line bodies use {}.

Matching Enums

match is at its most powerful when used with enums — it destructures each variant and binds its data to local names in one step. The compiler enforces that every variant is handled, so adding a new variant to an enum immediately produces compile errors in every match that needs updating.

#[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::Move { x, y }          => println!("move to ({}, {})", x, y),
        Message::Write(text)            => println!("write: {}", text),
        Message::ChangeColor(r, g, b)   => println!("color: #{:02X}{:02X}{:02X}", r, g, b),
    }
}

fn main() {
    process(Message::Move { x: 10, y: 20 });
    process(Message::Write(String::from("hello")));
    process(Message::ChangeColor(255, 128, 0));
    process(Message::Quit);
}
$ rustc pattern_matching_matching_enums.rs && ./pattern_matching_matching_enums
move to (10, 20)
write: hello
color: #FF8000
quit

Matching Option<T> and Result<T, E>

match is the standard way to handle Option and Result. The compiler ensures both the success and failure cases are addressed, eliminating the forgotten-null-check class of bugs.

fn safe_div(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    // Matching Option — handles both Some and None
    match safe_div(10.0, 3.0) {
        Some(result) => println!("{:.4}", result),
        None         => println!("division by zero"),
    }

    // Matching Result — handles both Ok and Err
    let parsed: Result<i32, _> = "42".parse();
    match parsed {
        Ok(n)  => println!("parsed: {}", n),
        Err(e) => println!("error: {}", e),
    }
}
$ rustc pattern_matching_matching_option_t_and_result_t_e.rs && ./pattern_matching_matching_option_t_and_result_t_e
3.3333
parsed: 42

Destructuring in Patterns

Pattern matching is not limited to enums — you can destructure tuples, structs, and nested data in any match arm, binding the inner values to names. This eliminates a lot of the field-access boilerplate you would otherwise write.

Tuples

fn main() {
    for point in [(0, 0), (4, 0), (0, -2), (3, -5)] {
        match point {
            (0, 0) => println!("origin"),
            (x, 0) => println!("on x-axis at {}", x),  // y matches 0, x is bound
            (0, y) => println!("on y-axis at {}", y),  // x matches 0, y is bound
            (x, y) => println!("at ({}, {})", x, y),   // both bound
        }
    }
}
$ rustc pattern_matching_tuples.rs && ./pattern_matching_tuples
origin
on x-axis at 4
on y-axis at -2
at (3, -5)

Arms are tried top to bottom and the first match wins — (0, 0) would also fit (x, 0), (0, y) and (x, y), but it never reaches them.

Structs

struct Point { x: i32, y: i32 }

fn main() {
    for p in [Point { x: 0, y: 9 }, Point { x: 3, y: 0 }, Point { x: 3, y: 7 }] {
        match p {
            Point { x: 0, y } => println!("on y-axis at {}", y),
            Point { x, y: 0 } => println!("on x-axis at {}", x),
            Point { x, y }    => println!("at ({}, {})", x, y),
        }
    }
}
$ rustc pattern_matching_structs.rs && ./pattern_matching_structs
on y-axis at 9
on x-axis at 3
at (3, 7)

Nested Enums

Patterns can be nested arbitrarily deep, which lets you match on deeply structured data in a single arm without intermediate variables:

enum Color {
    Rgb(u8, u8, u8),
    Hsv(u16, u8, u8),
}

enum Background {
    Solid(Color),
    Gradient(Color, Color),
}

fn main() {
    let backgrounds = vec![
        Background::Solid(Color::Rgb(255, 0, 128)),
        Background::Solid(Color::Hsv(200, 80, 90)),
        Background::Gradient(Color::Rgb(0, 0, 0), Color::Hsv(0, 0, 100)),
        Background::Gradient(Color::Rgb(9, 9, 9), Color::Hsv(120, 50, 50)),
        Background::Gradient(Color::Rgb(9, 9, 9), Color::Rgb(1, 2, 3)),
    ];

    for bg in backgrounds {
        match bg {
            // One arm can destructure through two levels of nesting
            Background::Solid(Color::Rgb(r, g, b)) =>
                println!("solid RGB: {} {} {}", r, g, b),
            Background::Solid(Color::Hsv(h, s, v)) =>
                println!("solid HSV: {} {} {}", h, s, v),
            Background::Gradient(Color::Rgb(0, 0, 0), _) =>
                println!("gradient from black"),
            Background::Gradient(_, Color::Hsv(h, _, _)) =>
                println!("gradient ending at hue {}", h),
            Background::Gradient(_, _) =>
                println!("gradient"),
        }
    }
}
$ rustc pattern_matching_nested_enums.rs && ./pattern_matching_nested_enums
solid RGB: 255 0 128
solid HSV: 200 80 90
gradient from black
gradient ending at hue 120
gradient

Background::Gradient(Color::Rgb(0, 0, 0), _) matches through two levels and pins literal values: only a gradient that starts at pure black takes that arm, whatever it ends in. The first gradient also ends in an HSV colour, but it never reaches the hue arm: earlier arms win. The plain Gradient(_, _) arm catches whatever is left — _ matches anything without binding it.

Match Guards

A match guard is an extra if condition appended after a pattern. The arm only fires if both the pattern matches and the guard condition is true. Guards let you express conditions that cannot be encoded in the pattern syntax alone.

fn main() {
    for num in [Some(-3), Some(0), Some(4), Some(7), None] {
        match num {
            Some(x) if x < 0  => println!("negative: {}", x),
            Some(x) if x == 0 => println!("zero"),
            Some(x) if x % 2 == 0 => println!("positive even: {}", x),
            Some(x)            => println!("positive odd: {}", x),
            None               => println!("none"),
        }
    }
}
$ rustc pattern_matching_match_guards.rs && ./pattern_matching_match_guards
negative: -3
zero
positive even: 4
positive odd: 7
none

The compiler does not look inside guards when checking exhaustiveness. Delete the plain Some(x) arm and the match no longer compiles, even though the four guarded arms cover every integer between them.

Binding with @

The @ operator lets you bind a name to a matched value while simultaneously applying a pattern to it. Without @ you would need either a guard (losing the range check in the type) or a separate variable. This is particularly useful with range patterns where you want both the constraint and the value.

fn main() {
    for age in [7, 17, 40, 81] {
        let category = match age {
            n @ 0..=12  => format!("child ({})", n),    // n is bound AND must be in 0..=12
            n @ 13..=17 => format!("teenager ({})", n),
            n @ 18..=64 => format!("adult ({})", n),
            n           => format!("senior ({})", n),   // catch-all, n is bound
        };

        println!("{}", category);
    }
}
$ rustc pattern_matching_binding_with.rs && ./pattern_matching_binding_with
child (7)
teenager (17)
adult (40)
senior (81)

if let — Single-Pattern Match

if let is syntactic sugar for a match with exactly one meaningful arm and an ignored catch-all. Use it when you care about one specific variant and want to silently skip everything else. It produces more readable code than a full match when only one case needs handling.

fn main() {
    let config: Option<&str> = Some("verbose");

    // Full match — wordy when you only care about one case
    match config {
        Some(val) => println!("config: {}", val),
        None => (), // explicitly doing nothing
    }

    // if let — cleaner for the single-case scenario
    if let Some(val) = config {
        println!("config: {}", val);
    }

    // if let with else — when the missing case also needs handling
    if let Some(val) = config {
        println!("set to: {}", val);
    } else {
        println!("not configured");
    }
}
$ rustc pattern_matching_if_let_single_pattern_match.rs && ./pattern_matching_if_let_single_pattern_match
config: verbose
config: verbose
set to: verbose

while let — Loop Until Pattern Fails

while let loops as long as a pattern matches, and exits cleanly when it does not. It is the idiomatic way to drain a stack, consume an iterator manually, or process items until a channel closes.

fn main() {
    let mut stack = vec![1, 2, 3, 4, 5];

    // Loops until pop() returns None (empty vec)
    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}
$ rustc pattern_matching_while_let_loop_until_pattern_fails.rs && ./pattern_matching_while_let_loop_until_pattern_fails
5
4
3
2
1

let and Function Parameters Destructure Too

Pattern matching is not limited to match, if let, and while let — it is built into let bindings and function parameters. This means you can destructure values wherever you introduce a binding.

fn print_point(&(x, y): &(i32, i32)) {
    // The parameter itself is a pattern — destructures the tuple reference on entry
    println!("({}, {})", x, y);
}

fn main() {
    // let with a tuple — destructures immediately
    let (a, b, c) = (1, 2, 3);
    println!("{} {} {}", a, b, c);

    // let with a struct — destructures named fields
    struct Point { x: i32, y: i32 }
    let Point { x, y } = Point { x: 5, y: 10 };
    println!("{} {}", x, y);

    // function parameter destructuring
    print_point(&(3, 4));
}
$ rustc pattern_matching_let_and_function_parameters_destructure_.rs && ./pattern_matching_let_and_function_parameters_destructure_
1 2 3
5 10
(3, 4)

Ignoring Values

Rust provides several ways to ignore parts of a matched value when you do not need them:

fn main() {
    let numbers = (1, 2, 3, 4, 5);

    // .. ignores all middle elements — matches first and last
    match numbers {
        (first, .., last) => println!("first: {}, last: {}", first, last),
    }

    // _ ignores specific positions
    let (_, second, _, fourth, _) = numbers;
    println!("{} {}", second, fourth);

    // Prefix _ suppresses the unused variable warning without binding
    let _unused = 42;
}
$ rustc pattern_matching_ignoring_values.rs && ./pattern_matching_ignoring_values
first: 1, last: 5
2 4

Practical: Parsing Commands

This example shows how match on a slice of string parts produces clean, readable command dispatch — a common pattern in CLI tools and simple interpreters:

#[derive(Debug)]
enum Command {
    Quit,
    Go(String),
    Set { key: String, value: String },
}

fn parse_command(input: &str) -> Option<Command> {
    let parts: Vec<&str> = input.trim().splitn(3, ' ').collect();
    match parts.as_slice() {
        ["quit"]           => Some(Command::Quit),
        ["go", dest]       => Some(Command::Go(dest.to_string())),
        ["set", key, val]  => Some(Command::Set {
            key: key.to_string(),
            value: val.to_string(),
        }),
        _ => None, // unrecognised input
    }
}

fn main() {
    let inputs = ["quit", "go north", "set speed 10", "unknown"];

    for input in &inputs {
        match parse_command(input) {
            Some(Command::Quit) => println!("bye"),
            Some(Command::Go(dest)) => println!("walking {}", dest),
            Some(Command::Set { key, value }) => println!("{} := {}", key, value),
            None => println!("unknown command: {}", input),
        }
    }
}
$ rustc pattern_matching_practical_parsing_commands.rs && ./pattern_matching_practical_parsing_commands
bye
walking north
speed := 10
unknown command: unknown

parts.as_slice() turns the Vec<&str> into a slice so its shape can be matched: ["go", dest] only fits a two-element slice whose first element is "go". splitn(3, ' ') caps the split at three parts, so set title hello world would still match ["set", key, val] with val = "hello world".

Practice

  1. In the match-guards example, change the last Some(x) arm to Some(x) if x % 2 != 0. Every integer is now covered by one of the guards — does it compile?

    Solution

    No:

    error[E0004]: non-exhaustive patterns: `Some(_)` not covered
    ...
      = note: match arms with guards don't count towards exhaustivity

    The compiler does not reason about what guards cover, so at least one unguarded Some(..) arm (or _) is required.

  2. In the tuples example, move the (x, y) arm to the top. What does the compiler warn about, and what prints for (0, 0)?

    Solution
    warning: unreachable pattern
    ...
    4 |             (x, y) => println!("at ({}, {})", x, y),
      |             ------ matches any value
    5 |             (0, 0) => println!("origin"),
      |             ^^^^^^ no value can reach this

    It still compiles, but (0, 0) prints at (0, 0) — the catch-all wins because it comes first.

  3. Feed "set title hello world" and "go" to parse_command. Predict which arms match.

    Solution

    "set title hello world" splits into at most three parts, ["set", "title", "hello world"], so it matches ["set", key, val] and prints title := hello world. "go" is the one-element slice ["go"], which fits neither ["quit"] nor ["go", dest], so it prints unknown command: go.

  4. Combine @ with an or-pattern: bind small only when a number is 1, 2 or 3, and try it on 2, 5 and 9.

    Solution
    match n {
        small @ (1 | 2 | 3) => println!("{} is small", small),
        other => println!("{} is not", other),
    }

    Prints 2 is small, 5 is not, 9 is not. The parentheses are needed so @ binds the whole alternation, not just the 1.

Frequently Asked Questions

Does match in Rust need to be exhaustive?
Yes. Every possible value must be handled. The compiler rejects non-exhaustive matches. Use _ as a catch-all arm.
What is if let?
if let is syntactic sugar for a match with one pattern and an optional else. Use it when you only care about one variant.
Can you match on multiple patterns at once?
Yes. Use the | operator to match multiple patterns in one arm: 1 | 2 | 3 => ...