Guide contents

Lesson 23 of 94

Pattern Matching in Depth

You have already used patterns in match, let, if let, and for. This lesson collects every kind of pattern in one place, from the simplest to the ones that make Rust code short and precise.

Rust's patterns cover a lot: ranges, slices, nested destructuring, bindings with @, and guaranteed exhaustiveness everywhere.

Concept Rust
Matching an enum variant Shape::Circle(r) =>
Destructuring a struct Point { x, y } =>
Guard condition n if n > 10 =>
Catch-all _ =>
Testing for one variant if let Value::Str(s) = value
Ranges, slices, and bindings Ranges 1..=9, slices [first, .., last], bindings n @ 1..=9

Where patterns appear

Patterns are not just for match. They appear in:

  • match arms,
  • let statements (let (a, b) = pair;),
  • function and closure parameters (fn f((x, y): (i32, i32))),
  • if let, while let, and let ... else,
  • for loops (for (i, item) in ...).

1. Literal and multiple-value patterns

Rust
fn main() {
    for code in [0, 1, 2, 3, 99] {
        let meaning = match code {
            0 => "success",
            1 | 2 => "retryable error",
            3 => "fatal error",
            _ => "unknown",
        };
        println!("{code}: {meaning}");
    }
}
Output
0: success
1: retryable error
2: retryable error
3: fatal error
99: unknown

2. Range patterns for numbers and characters

Rust
fn classify(c: char) -> &'static str {
    match c {
        'a'..='z' => "lowercase",
        'A'..='Z' => "uppercase",
        '0'..='9' => "digit",
        ' ' | '\t' | '\n' => "whitespace",
        _ => "other",
    }
}

fn main() {
    for c in ['q', 'Q', '7', ' ', '€'] {
        println!("{c:?} is {}", classify(c));
    }
}
Output
'q' is lowercase
'Q' is uppercase
'7' is digit
' ' is whitespace
'€' is other

3. Binding a matched value with @

name @ pattern tests the pattern and also gives the matched value a name.

Rust
fn describe_age(age: u32) -> String {
    match age {
        0 => String::from("newborn"),
        child @ 1..=12 => format!("child of {child}"),
        teen @ 13..=19 => format!("teenager of {teen}"),
        adult => format!("adult of {adult}"),
    }
}

fn main() {
    for a in [0, 7, 16, 42] {
        println!("{}", describe_age(a));
    }
}
Output
newborn
child of 7
teenager of 16
adult of 42

4. Destructuring tuples

Rust
fn main() {
    let points = [(0, 0), (3, 0), (0, -2), (4, 5)];
    for p in points {
        let where_ = match p {
            (0, 0) => String::from("origin"),
            (x, 0) => format!("on the x-axis at {x}"),
            (0, y) => format!("on the y-axis at {y}"),
            (x, y) => format!("at ({x}, {y})"),
        };
        println!("{where_}");
    }
}
Output
origin
on the x-axis at 3
on the y-axis at -2
at (4, 5)

5. Destructuring structs

Fields can be matched against literals, bound to variables, renamed, or ignored with ...

Rust
struct Order {
    id: u32,
    country: &'static str,
    total_cents: u64,
    express: bool,
}

fn shipping_cents(o: &Order) -> u64 {
    match o {
        Order { express: true, country: "US", .. } => 1_500,
        Order { express: true, .. } => 3_500,
        Order { country: "US", total_cents: t, .. } if *t >= 5_000 => 0,
        Order { country: "US", .. } => 500,
        Order { total_cents, .. } => 1_000 + total_cents / 20,
    }
}

fn main() {
    let orders = [
        Order { id: 1, country: "US", total_cents: 2_000, express: true },
        Order { id: 2, country: "DE", total_cents: 2_000, express: true },
        Order { id: 3, country: "US", total_cents: 7_500, express: false },
        Order { id: 4, country: "US", total_cents: 1_000, express: false },
        Order { id: 5, country: "JP", total_cents: 10_000, express: false },
    ];
    for o in &orders {
        println!("order {}: shipping {}", o.id, shipping_cents(o));
    }
}
Output
order 1: shipping 1500
order 2: shipping 3500
order 3: shipping 0
order 4: shipping 500
order 5: shipping 1500

total_cents: t binds the field under a new name; total_cents alone binds it under its own name.

6. Nested patterns

Patterns can nest as deeply as the data does.

Rust
enum Payment {
    Card { network: &'static str, last4: &'static str },
    Wallet(&'static str),
}

enum Status {
    Paid(Payment),
    Failed { reason: &'static str, retryable: bool },
    Pending,
}

fn summary(s: &Status) -> String {
    match s {
        Status::Paid(Payment::Card { network: "amex", .. }) => String::from("paid with Amex"),
        Status::Paid(Payment::Card { network, last4 }) => format!("paid with {network} ending {last4}"),
        Status::Paid(Payment::Wallet(name)) => format!("paid with {name}"),
        Status::Failed { retryable: true, reason } => format!("failed ({reason}), will retry"),
        Status::Failed { reason, .. } => format!("failed permanently: {reason}"),
        Status::Pending => String::from("pending"),
    }
}

fn main() {
    let statuses = [
        Status::Paid(Payment::Card { network: "visa", last4: "4242" }),
        Status::Paid(Payment::Card { network: "amex", last4: "0005" }),
        Status::Paid(Payment::Wallet("Apple Pay")),
        Status::Failed { reason: "timeout", retryable: true },
        Status::Failed { reason: "card stolen", retryable: false },
        Status::Pending,
    ];
    for s in &statuses {
        println!("{}", summary(s));
    }
}
Output
paid with visa ending 4242
paid with Amex
paid with Apple Pay
failed (timeout), will retry
failed permanently: card stolen
pending

7. Ignoring values: _, .., and _name

  • _ matches anything and does not bind it.
  • .. skips the remaining fields or elements.
  • _name binds the value but silences the "unused variable" warning.
Rust
fn main() {
    let record = (42, "ferris", "crab@example.com", true, 8);
    let (id, _, email, ..) = record;
    println!("{id} {email}");

    let (first, .., last) = record;
    println!("{first} {last}");

    let _unused_for_now = record.3;
}
Output
42 crab@example.com
42 8

8. Match guards with several conditions

A guard can use any boolean expression, including the bound variables. A guard applies to the whole arm, including all | alternatives.

Rust
fn triage(cpu: u32, memory: u32, is_prod: bool) -> &'static str {
    match (cpu, memory) {
        (c, m) if is_prod && (c > 90 || m > 90) => "page on-call",
        (c, m) if c > 90 || m > 90 => "open a ticket",
        (c, _) | (_, c) if c > 75 => "watch",
        _ => "ok",
    }
}

fn main() {
    println!("{}", triage(95, 40, true));
    println!("{}", triage(95, 40, false));
    println!("{}", triage(20, 80, true));
    println!("{}", triage(20, 30, true));
}
Output
page on-call
open a ticket
watch
ok

9. A variable name in a pattern *binds*, it does not compare

This is a common pattern mistake. A plain name in a pattern creates a new variable that matches anything. It does not compare against an existing variable with that name.

Rust
fn main() {
    let expected_status = 200;
    let actual = 404;
    match actual {
        expected_status => println!("matched! (actually bound {expected_status})"),
    }
}
Output
matched! (actually bound 404)

The compiler warns about this (irrefutable pattern / unreachable pattern when there are more arms). To compare with a variable, use a guard; to compare with a constant, use a const, whose uppercase name works in patterns:

Rust
const OK: u16 = 200;

fn main() {
    let expected_status: u16 = 404;
    for actual in [200, 404, 500] {
        match actual {
            OK => println!("{actual}: ok"),
            s if s == expected_status => println!("{actual}: the expected status"),
            _ => println!("{actual}: something else"),
        }
    }
}
Output
200: ok
404: the expected status
500: something else

10. Refutable and irrefutable patterns

A pattern that always matches, like (a, b) for a tuple, is irrefutable and can be used with plain let. A pattern that might fail, like Some(x), is refutable. Using a refutable pattern with plain let is an error, because let has nowhere to go if it fails.

Rust✗ does not compile · E0005
fn main() {
    let maybe_port: Option<u16> = None;
    let Some(port) = maybe_port;
    println!("{port}");
}
Output
error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Some(port) = maybe_port;
  |         ^^^^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `Option<u16>`
help: you might want to use `let...else` to handle the variant that isn't matched
  |
3 |     let Some(port) = maybe_port else { todo!() };
  |                                 ++++++++++++++++

Use if let, let ... else, or match for refutable patterns.

11. if let ... else if let ... else

Rust
enum Input {
    Key(char),
    Click(i32, i32),
    Scroll(i32),
}

fn main() {
    let inputs = [Input::Scroll(-3), Input::Key('q'), Input::Click(5, 9)];
    for input in &inputs {
        if let Input::Key(c) = input {
            println!("key {c}");
        } else if let Input::Click(x, y) = input {
            println!("click {x},{y}");
        } else {
            println!("something else");
        }
    }
}
Output
something else
key q
click 5,9

12. if let chains with &&

Since the 2024 edition, if let can be combined with other conditions and other let patterns using &&. This removes a lot of nesting.

Rust
fn main() {
    let raw_port: Option<&str> = Some("8443");
    let use_tls = true;

    if let Some(text) = raw_port
        && let Ok(port) = text.parse::<u16>()
        && use_tls
        && port != 80
    {
        println!("starting TLS listener on {port}");
    } else {
        println!("using defaults");
    }
}
Output
starting TLS listener on 8443

13. while let for loops that end on a pattern

while let keeps looping as long as the pattern matches. It is ideal for draining a stack or a queue.

Rust
fn main() {
    let mut pending = vec!["compile", "test", "package"];
    while let Some(step) = pending.pop() {
        println!("running {step}");
    }
    println!("done, {} left", pending.len());
}
Output
running package
running test
running compile
done, 0 left

14. let ... else keeps the happy path flat

Rust
fn parse_header(line: &str) -> String {
    let Some((name, value)) = line.split_once(':') else {
        return format!("malformed: {line:?}");
    };
    let [first, ..] = name.trim().as_bytes() else {
        return String::from("empty header name");
    };
    format!("{} (starts with {:?}) = {}", name.trim(), *first as char, value.trim())
}

fn main() {
    println!("{}", parse_header("Content-Type: text/html"));
    println!("{}", parse_header("no colon here"));
    println!("{}", parse_header(" : value"));
}
Output
Content-Type (starts with 'C') = text/html
malformed: "no colon here"
empty header name

15. matches! for a quick yes/no

Rust
#[derive(Debug)]
enum Role {
    Admin,
    Editor { sections: u8 },
    Viewer,
}

fn main() {
    let roles = [Role::Viewer, Role::Editor { sections: 3 }, Role::Admin, Role::Editor { sections: 0 }];
    for r in &roles {
        let can_write = matches!(r, Role::Admin | Role::Editor { sections: 1.. });
        println!("{:?}: can write = {can_write}", r);
    }
}
Output
Viewer: can write = false
Editor { sections: 3 }: can write = true
Admin: can write = true
Editor { sections: 0 }: can write = false

1.. is a half-open range pattern: "1 or more".

16. Slice patterns

Slices and arrays can be matched by shape. rest @ .. binds the middle part as a sub-slice.

Rust
fn summarize(values: &[i32]) -> String {
    match values {
        [] => String::from("empty"),
        [x] => format!("just {x}"),
        [a, b] => format!("a pair: {a} and {b}"),
        [first, rest @ ..] if rest.len() > 3 => format!("{first} and {} more", rest.len()),
        [first, .., last] => format!("from {first} to {last}"),
    }
}

fn main() {
    println!("{}", summarize(&[]));
    println!("{}", summarize(&[7]));
    println!("{}", summarize(&[1, 2]));
    println!("{}", summarize(&[1, 2, 3]));
    println!("{}", summarize(&[1, 2, 3, 4, 5, 6]));
}
Output
empty
just 7
a pair: 1 and 2
from 1 to 3
1 and 5 more

17. Matching the start of a slice

Rust
fn is_png(bytes: &[u8]) -> bool {
    matches!(bytes, [0x89, b'P', b'N', b'G', ..])
}

fn describe(path_parts: &[&str]) -> String {
    match path_parts {
        ["api", "v1", rest @ ..] => format!("v1 API call: {}", rest.join("/")),
        ["api", version, ..] => format!("unsupported API version {version}"),
        ["static", file] => format!("static file {file}"),
        _ => String::from("not found"),
    }
}

fn main() {
    println!("{}", is_png(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A]));
    println!("{}", is_png(b"GIF89a"));
    println!("{}", describe(&["api", "v1", "users", "7"]));
    println!("{}", describe(&["api", "v2", "users"]));
    println!("{}", describe(&["static", "logo.svg"]));
    println!("{}", describe(&["admin"]));
}
Output
true
false
v1 API call: users/7
unsupported API version v2
static file logo.svg
not found

18. Matching a String against literals

A String can't be matched against &str literals directly. Match on s.as_str() instead.

Rust
fn content_type(extension: &str) -> &'static str {
    match extension.to_lowercase().as_str() {
        "html" | "htm" => "text/html",
        "css" => "text/css",
        "js" | "mjs" => "text/javascript",
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        _ => "application/octet-stream",
    }
}

fn main() {
    for ext in ["HTML", "js", "JPEG", "exe"] {
        println!("{ext}: {}", content_type(ext));
    }
}
Output
HTML: text/html
js: text/javascript
JPEG: image/jpeg
exe: application/octet-stream

19. Matching references: & patterns

When you match on a reference, a & in the pattern takes the reference apart and copies out the value (for Copy types).

Rust
fn main() {
    let scores = vec![90, 45, 72];

    for &s in &scores {          // s: i32
        print!("{} ", s + 1);
    }
    println!();

    match scores.first() {       // Option<&i32>
        Some(&top) if top >= 90 => println!("top score {top} is excellent"),
        Some(top) => println!("top score {top}"),
        None => println!("no scores"),
    }
}
Output
91 46 73
top score 90 is excellent

20. Default binding modes

When you match a reference with a pattern that has no &, Rust automatically makes every binding a reference. This is why matching on &Enum or &Struct never moves anything.

Rust
struct Config {
    name: String,
    retries: u32,
}

fn main() {
    let c = Config { name: String::from("worker"), retries: 3 };
    let Config { name, retries } = &c;   // name: &String, retries: &u32
    println!("{name} {retries}");
    println!("c is untouched: {} {}", c.name, c.retries);

    let mut m = Config { name: String::from("api"), retries: 0 };
    let Config { name, retries } = &mut m; // name: &mut String, retries: &mut u32
    name.push_str("-v2");
    *retries += 5;
    println!("{} {}", m.name, m.retries);
}
Output
worker 3
c is untouched: worker 3
api-v2 5

21. The ref keyword

Before default binding modes existed, you wrote ref in a pattern to bind by reference instead of by move. You will still see it in older code, and it is sometimes needed when matching on an owned value that you don't want to move.

Rust
fn main() {
    let maybe_name: Option<String> = Some(String::from("Ferris"));
    match maybe_name {
        Some(ref name) => println!("hello {name}"), // borrow, don't move
        None => println!("hello stranger"),
    }
    println!("still usable: {:?}", maybe_name);
}
Output
hello Ferris
still usable: Some("Ferris")

Without ref, Some(name) would move the String out of maybe_name. Matching on &maybe_name is the modern way to write the same thing.

22. Patterns in function parameters

Rust
fn distance((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 {
    ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}

struct Size {
    width: u32,
    height: u32,
}

fn aspect(&Size { width, height }: &Size) -> f64 {
    width as f64 / height as f64
}

fn main() {
    println!("{}", distance((0.0, 0.0), (3.0, 4.0)));
    println!("{:.3}", aspect(&Size { width: 1920, height: 1080 }));
}
Output
5
1.778

23. Patterns in for loops

Rust
struct Sale {
    region: &'static str,
    amount: u32,
}

fn main() {
    let sales = [
        Sale { region: "north", amount: 120 },
        Sale { region: "south", amount: 340 },
    ];
    for (rank, Sale { region, amount }) in sales.iter().enumerate() {
        println!("#{} {region}: {amount}", rank + 1);
    }

    let pairs = vec![("a", 1), ("b", 2)];
    for &(letter, number) in &pairs {
        println!("{letter}{number}");
    }
}
Output
#1 north: 120
#2 south: 340
a1
b2

24. Exhaustiveness across several values

The compiler checks exhaustiveness for any combination, not just single enums. Matching two booleans needs all four cases covered.

Rust
fn access(logged_in: bool, is_admin: bool) -> &'static str {
    match (logged_in, is_admin) {
        (false, _) => "please log in",
        (true, true) => "admin dashboard",
        (true, false) => "user dashboard",
    }
}

fn main() {
    println!("{}", access(false, true));
    println!("{}", access(true, true));
    println!("{}", access(true, false));
}
Output
please log in
admin dashboard
user dashboard

25. Realistic example: rock, paper, scissors

Rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum Hand {
    Rock,
    Paper,
    Scissors,
}

#[derive(Debug)]
enum Outcome {
    Win,
    Lose,
    Draw,
}

fn play(me: Hand, them: Hand) -> Outcome {
    use Hand::*;
    match (me, them) {
        (a, b) if a == b => Outcome::Draw,
        (Rock, Scissors) | (Paper, Rock) | (Scissors, Paper) => Outcome::Win,
        _ => Outcome::Lose,
    }
}

fn main() {
    let rounds = [(Hand::Rock, Hand::Scissors), (Hand::Rock, Hand::Paper), (Hand::Paper, Hand::Paper)];
    for (me, them) in rounds {
        println!("{:?} vs {:?}: {:?}", me, them, play(me, them));
    }
}
Output
Rock vs Scissors: Win
Rock vs Paper: Lose
Paper vs Paper: Draw

26. Realistic example: an HTTP router

Splitting the path into segments and matching on (method, segments) gives a readable router with no regular expressions.

Rust
fn route(method: &str, path: &str) -> String {
    let segments: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
    match (method, segments.as_slice()) {
        ("GET", []) => String::from("home page"),
        ("GET", ["users"]) => String::from("list users"),
        ("POST", ["users"]) => String::from("create user"),
        ("GET", ["users", id]) => format!("show user {id}"),
        ("DELETE", ["users", id]) => format!("delete user {id}"),
        ("GET", ["users", id, "orders", rest @ ..]) => format!("orders of user {id}, filter {:?}", rest),
        (_, ["users", ..]) => String::from("405 method not allowed"),
        _ => String::from("404 not found"),
    }
}

fn main() {
    let requests = [
        ("GET", "/"),
        ("GET", "/users"),
        ("POST", "/users/"),
        ("GET", "/users/42"),
        ("DELETE", "/users/42"),
        ("GET", "/users/42/orders/2026/09"),
        ("PUT", "/users/42"),
        ("GET", "/nope"),
    ];
    for (m, p) in requests {
        println!("{m:<6} {p:<28} -> {}", route(m, p));
    }
}
Output
GET    /                            -> home page
GET    /users                       -> list users
POST   /users/                      -> create user
GET    /users/42                    -> show user 42
DELETE /users/42                    -> delete user 42
GET    /users/42/orders/2026/09     -> orders of user 42, filter ["2026", "09"]
PUT    /users/42                    -> 405 method not allowed
GET    /nope                        -> 404 not found

27. Realistic example: command-line argument parsing

Rust
#[derive(Debug)]
enum Cli {
    Help,
    Version,
    Build { release: bool, target: Option<String> },
    Run { args: Vec<String> },
    Invalid(String),
}

fn parse(args: &[&str]) -> Cli {
    match args {
        [] | ["help"] | ["-h"] | ["--help"] => Cli::Help,
        ["--version"] | ["-V"] => Cli::Version,
        ["build"] => Cli::Build { release: false, target: None },
        ["build", "--release"] => Cli::Build { release: true, target: None },
        ["build", "--target", t] | ["build", "--release", "--target", t] => Cli::Build {
            release: args.contains(&"--release"),
            target: Some(t.to_string()),
        },
        ["run", "--", rest @ ..] => Cli::Run { args: rest.iter().map(|s| s.to_string()).collect() },
        [other, ..] => Cli::Invalid(format!("unknown command '{other}'")),
    }
}

fn main() {
    let inputs: [&[&str]; 7] = [
        &[],
        &["--version"],
        &["build", "--release"],
        &["build", "--release", "--target", "wasm32"],
        &["run", "--", "--port", "8080"],
        &["deploy"],
        &["build", "--target", "x86_64"],
    ];
    for args in inputs {
        println!("{:<45} {:?}", format!("{:?}", args), parse(args));
    }
}
Output
[]                                            Help
["--version"]                                 Version
["build", "--release"]                        Build { release: true, target: None }
["build", "--release", "--target", "wasm32"]  Build { release: true, target: Some("wasm32") }
["run", "--", "--port", "8080"]               Run { args: ["--port", "8080"] }
["deploy"]                                    Invalid("unknown command 'deploy'")
["build", "--target", "x86_64"]               Build { release: false, target: Some("x86_64") }

28. Realistic example: a lexer

Character patterns with ranges and guards turn text into tokens.

Rust
#[derive(Debug, PartialEq)]
enum Token {
    Number(i64),
    Ident(String),
    Op(char),
    LParen,
    RParen,
}

fn lex(src: &str) -> Vec<Token> {
    let chars: Vec<char> = src.chars().collect();
    let mut tokens = Vec::new();
    let mut i = 0;
    while i < chars.len() {
        match chars[i] {
            ' ' | '\t' => i += 1,
            '(' => { tokens.push(Token::LParen); i += 1; }
            ')' => { tokens.push(Token::RParen); i += 1; }
            op @ ('+' | '-' | '*' | '/' | '=') => { tokens.push(Token::Op(op)); i += 1; }
            '0'..='9' => {
                let start = i;
                while i < chars.len() && chars[i].is_ascii_digit() {
                    i += 1;
                }
                let text: String = chars[start..i].iter().collect();
                tokens.push(Token::Number(text.parse().unwrap()));
            }
            c if c.is_alphabetic() || c == '_' => {
                let start = i;
                while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
                    i += 1;
                }
                tokens.push(Token::Ident(chars[start..i].iter().collect()));
            }
            other => panic!("unexpected character {other:?}"),
        }
    }
    tokens
}

fn main() {
    for t in lex("total = (price_1 + 42) * qty") {
        println!("{:?}", t);
    }
}
Output
Ident("total")
Op('=')
LParen
Ident("price_1")
Op('+')
Number(42)
RParen
Op('*')
Ident("qty")

op @ ('+' | '-' | ...) binds whichever operator matched. The unwrap() is safe because the characters were all checked to be digits.

29. Realistic example: normalizing a date in several formats

Rust
fn normalize(date: &str) -> Option<String> {
    let parts: Vec<&str> = date.split(|c| c == '-' || c == '/' || c == '.').collect();
    let nums: Vec<u32> = parts.iter().map(|p| p.parse().ok()).collect::<Option<Vec<u32>>>()?;
    let (y, m, d) = match (parts.as_slice(), nums.as_slice()) {
        ([y, _, _], &[year, month, day]) if y.len() == 4 => (year, month, day), // 2026-09-27
        ([_, _, y], &[day, month, year]) if y.len() == 4 && date.contains('.') => (year, month, day), // 27.09.2026
        ([_, _, y], &[month, day, year]) if y.len() == 4 => (year, month, day), // 09/27/2026
        _ => return None,
    };
    match (m, d) {
        (1..=12, 1..=31) => Some(format!("{y:04}-{m:02}-{d:02}")),
        _ => None,
    }
}

fn main() {
    for input in ["2026-09-27", "27.09.2026", "09/27/2026", "2026-13-01", "27/9", "tomorrow"] {
        println!("{input:>11} -> {:?}", normalize(input));
    }
}
Output
 2026-09-27 -> Some("2026-09-27")
 27.09.2026 -> Some("2026-09-27")
 09/27/2026 -> Some("2026-09-27")
 2026-13-01 -> None
       27/9 -> None
   tomorrow -> None

30. Realistic example: summarizing a batch of job results

Nested enums plus guards and @ bindings classify results precisely.

Rust
enum JobResult {
    Success { duration_ms: u64 },
    Failure { code: i32, attempts: u32 },
    Skipped(&'static str),
}

fn main() {
    let results = [
        JobResult::Success { duration_ms: 120 },
        JobResult::Success { duration_ms: 9_500 },
        JobResult::Failure { code: 137, attempts: 1 },
        JobResult::Failure { code: 1, attempts: 5 },
        JobResult::Skipped("no changes"),
    ];

    let (mut fast, mut slow, mut failed) = (0, 0, 0);
    for r in &results {
        match r {
            JobResult::Success { duration_ms: d @ 0..=999 } => { fast += 1; println!("fast success in {d}ms"); }
            JobResult::Success { duration_ms } => { slow += 1; println!("slow success in {duration_ms}ms"); }
            JobResult::Failure { code: 137, .. } => { failed += 1; println!("killed: out of memory"); }
            JobResult::Failure { code, attempts } if *attempts >= 5 => { failed += 1; println!("gave up after {attempts} attempts (exit {code})"); }
            JobResult::Failure { code, .. } => { failed += 1; println!("failed with exit {code}, will retry"); }
            JobResult::Skipped(why) => println!("skipped: {why}"),
        }
    }
    println!("fast={fast} slow={slow} failed={failed}");
}
Output
fast success in 120ms
slow success in 9500ms
killed: out of memory
gave up after 5 attempts (exit 1)
skipped: no changes
fast=1 slow=1 failed=2

Summary

  • Patterns appear in match, let, if let, while let, let ... else, function parameters, and for loops.
  • Patterns can be literals, ranges (1..=9, 'a'..='z'), alternatives (a | b), wildcards (_, ..), and destructurings of tuples, structs, enums, arrays, and slices.
  • name @ pattern binds while testing. Guards (if cond) add arbitrary conditions.
  • A bare name in a pattern binds, it never compares. Use a guard or a const.
  • Plain let needs an irrefutable pattern; use if let, let ... else, or match for refutable ones.
  • if let chains (if let A = a && let B = b && cond) remove nesting.
  • Matching on a reference binds references automatically.

Next: Result & the ? Operator, Rust's replacement for exceptions.