Enums & Pattern Matching

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. An enum defines a type by listing its possible variants, each of which may carry data. Combined with match and its exhaustiveness check, enums are Rust’s main tool for modelling "one of several shapes" — results, messages, states, syntax trees — and for making invalid states unrepresentable. This page covers enums and the full pattern language; the shorthand constructs if let, let-else and let chains are on Control Flow.

Defining enums

Variants can be unit-like, tuple-like or struct-like, and different variants can carry different data:

#[derive(Debug, Clone, PartialEq)]
enum Format {
    Hardcover,                                // unit variant
    Paperback,
    Ebook { file_type: String, drm: bool },   // struct-like variant
    Audiobook(u32),                           // tuple-like variant: minutes
}

impl Format {
    fn is_physical(&self) -> bool {
        matches!(self, Format::Hardcover | Format::Paperback)
    }
}

fn main() {
    let formats = vec![
        Format::Hardcover,
        Format::Ebook { file_type: "epub".into(), drm: false },
        Format::Audiobook(1_260),
    ];
    for f in &formats {
        println!("{f:?} physical={}", f.is_physical());
    }
}

An enum value is the size of its largest variant plus a discriminant (tag) that records which variant it is — often less, thanks to niche optimisation (see Memory Layout & repr). Fieldless enums can be given explicit discriminants and cast to integers:

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
enum Condition {
    Poor = 1,
    Fair = 2,
    Good = 3,
    LikeNew = 4,
}

fn main() {
    println!("{} {}", Condition::Good as u8, Condition::LikeNew > Condition::Fair); // 3 true
}

Option is an enum

Rust has no null. A value that may be absent has type Option<T>, an ordinary enum from the standard library (in the prelude, together with its variants):

enum MyOption<T> { // the standard library's definition, renamed
    None,
    Some(T),
}

Because Option<T> and T are different types, the compiler forces you to handle the None case before using the value — the "billion-dollar mistake" of null references cannot happen by accident. Result<T, E> is the same idea for operations that can fail. Their APIs are on Option, Result & Combinators.

match and exhaustiveness

match compares a value against a list of patterns, top to bottom, and runs the arm of the first that matches. It is an expression; all arms must have the same type. The compiler checks that the arms are exhaustive — every possible value matches some arm — and reports the missing patterns otherwise, with error E0004:

enum Format {
    Hardcover,
    Paperback,
    Ebook,
}

fn shipping_cents(f: Format) -> u32 {
    match f { // error[E0004]: non-exhaustive patterns: `Format::Ebook` not covered
        Format::Hardcover => 450,
        Format::Paperback => 300,
    }
}

fn main() {
    println!("{}", shipping_cents(Format::Hardcover));
}

Exhaustiveness is what makes enums safe to evolve: add a variant, and the compiler lists every match that needs updating. A wildcard arm _ => ... defeats that check, so prefer listing variants explicitly in your own code.

#[derive(Debug)]
enum Format {
    Hardcover,
    Paperback,
    Ebook { drm: bool },
    Audiobook(u32),
}

fn describe(f: &Format) -> String {
    match f {
        Format::Hardcover | Format::Paperback => "printed".to_string(),
        Format::Ebook { drm: true } => "ebook (DRM)".to_string(),
        Format::Ebook { drm: false } => "ebook".to_string(),
        Format::Audiobook(minutes) if *minutes > 600 => format!("long audiobook ({} h)", minutes / 60),
        Format::Audiobook(minutes) => format!("audiobook ({minutes} min)"),
    }
}

fn main() {
    for f in [Format::Paperback, Format::Ebook { drm: true }, Format::Audiobook(1260), Format::Audiobook(90)] {
        println!("{}", describe(&f));
    }
}

Patterns

Patterns appear in match arms, let, if let, while let, for loops and function parameters. The full syntax:

Pattern Matches Example

Literal

an exact value

0, 'a', "dune", true

Identifier

anything; binds it to a name

x, mut x, ref x

Wildcard

anything; binds nothing

_

Range

a value within an inclusive or exclusive range (integers and char)

1..=5, 'a'..='z', 0..10 (1.80), 100..

Tuple

a tuple, element by element

(0, y), (x, _, _)

Struct

a struct or struct-like variant by field

Point { x: 0, y }, Book { year, .. }

Tuple struct / variant

positional fields

Some(x), Rgb(r, _, _)

Slice / array

by length and position

[], [x], [first, .., last], [head, tail @ ..]

Rest

the remaining elements or fields

..

Or

any of several patterns

1 | 2, Some(0) | None

Binding with @

binds the value while also testing it

n @ 1..=9, whole @ Some(_)

Reference

dereferences a reference

&x, &(a, b)

Path

a constant or unit variant

Format::Ebook, MAX_LOANS

A guard (if condition after the pattern) adds a boolean test; since 1.95 the guard can also be an if let (see Control Flow). The patterns in action:

fn classify_year(year: u16) -> &'static str {
    match year {
        0..1450 => "manuscript era",        // exclusive range pattern (1.80)
        1450..=1799 => "early print",
        1800..=1999 => "modern",
        2000.. => "contemporary",           // half-open range
    }
}

fn shelf_code(code: &str) -> String {
    match code.as_bytes() {
        [] => "empty".to_string(),
        [b'R', rest @ ..] => format!("reference, {} more chars", rest.len()),
        [first, .., last] if first == last => "palindromic code".to_string(),
        [single] => format!("single char {}", *single as char),
        [first, ..] => format!("starts with {}", *first as char),
    }
}

fn rating_label(stars: u8) -> String {
    match stars {
        0 => "unrated".to_string(),
        n @ 1..=2 => format!("{n}: poor"),
        n @ (3 | 4) => format!("{n}: good"),
        5 => "5: excellent".to_string(),
        n => format!("{n}: invalid"),
    }
}

struct Loan {
    member: String,
    days: u16,
    renewed: bool,
}

fn loan_status(loan: &Loan) -> String {
    match loan {
        Loan { days: 0, .. } => "due today".to_string(),
        Loan { days: 1..=3, renewed: false, member } => format!("{member}: renew soon"),
        Loan { days, .. } => format!("{days} days left"),
    }
}

fn main() {
    for y in [1200, 1605, 1965, 2021] {
        print!("{} | ", classify_year(y));
    }
    println!();
    for c in ["", "R12", "ABA", "Q", "FIC"] {
        print!("{} | ", shelf_code(c));
    }
    println!();
    for s in [0, 2, 4, 5, 9] {
        print!("{} | ", rating_label(s));
    }
    println!();
    let loan = Loan { member: "alice".into(), days: 2, renewed: false };
    println!("{}", loan_status(&loan));
}

Match arms are checked in order, so put specific patterns before general ones. An arm that can never match because an earlier arm already covers everything it would (an unreachable pattern) is a warning. Errors for struct and tuple patterns: E0023 (wrong number of fields in a tuple-variant pattern), E0026 (no such field), E0027 (a field is missing and there is no ..).

Refutable and irrefutable patterns

A pattern that matches every possible value is irrefutable (x, (a, b), Point { x, y }); one that can fail is refutable (Some(x), 1..=5). let, function parameters and for loops require irrefutable patterns; if let, while let, let-else and match arms accept refutable ones. Using a refutable pattern in a plain let is error E0005:

fn main() {
    let maybe_year: Option<u16> = Some(1965);
    let Some(year) = maybe_year; // error[E0005]: refutable pattern in local binding
    println!("{year}");
}

The fix is let Some(year) = maybe_year else { return; }; or an if let.

matches! and assert_matches!

matches!(value, pattern) evaluates to true if the pattern matches — a compact boolean test, with an optional guard. std::assert_matches! (stable since 1.96; it is not in the prelude, so call it by path or import it with use std::assert_matches;) asserts that a value matches a pattern and, on failure, prints the value with its Debug representation — clearer than assert!(matches!(...)) in tests:

#[derive(Debug)]
enum Lookup {
    Found { title: String, copies: u8 },
    NotFound,
}

fn main() {
    let result = Lookup::Found { title: "Dune".into(), copies: 2 };
    let available = matches!(result, Lookup::Found { copies: 1.., .. });
    println!("{available}"); // true

    std::assert_matches!(result, Lookup::Found { ref title, .. } if title == "Dune");
    assert!(!matches!(Lookup::NotFound, Lookup::Found { .. }));
}

Source: std::matches

Enums as state machines: LoanState

Enums are a natural fit for state machines: each variant is a state carrying exactly the data valid in that state, and transitions are methods that consume one state and return the next. The shelf catalogue tracks loans this way.

stateDiagram-v2 [*] --> Requested: member asks for a book Requested --> Active: book is available Requested --> Cancelled: member withdraws Active --> Active: renew (max 3 times) Active --> Overdue: due date passes Overdue --> Returned: book handed back, fee charged Active --> Returned: book handed back Cancelled --> [*] Returned --> [*]
#[derive(Debug, Clone, PartialEq)]
enum LoanState {
    Requested { member: String },
    Active { member: String, due_day: u32, renewals: u8 },
    Overdue { member: String, days_late: u32 },
    Returned { fee_cents: u32 },
    Cancelled,
}

impl LoanState {
    fn approve(self, today: u32) -> Self {
        match self {
            LoanState::Requested { member } => LoanState::Active { member, due_day: today + 21, renewals: 0 },
            other => other, // any other state: no change
        }
    }

    fn renew(self) -> Result<Self, (Self, &'static str)> {
        match self {
            LoanState::Active { member, due_day, renewals } if renewals < 3 => {
                Ok(LoanState::Active { member, due_day: due_day + 21, renewals: renewals + 1 })
            }
            s @ LoanState::Active { .. } => Err((s, "renewal limit reached")),
            s => Err((s, "only active loans can be renewed")),
        }
    }

    fn tick(self, today: u32) -> Self {
        match self {
            LoanState::Active { member, due_day, .. } if today > due_day => {
                LoanState::Overdue { member, days_late: today - due_day }
            }
            other => other,
        }
    }

    fn give_back(self) -> Self {
        match self {
            LoanState::Active { .. } => LoanState::Returned { fee_cents: 0 },
            LoanState::Overdue { days_late, .. } => LoanState::Returned { fee_cents: days_late.min(10) * 50 },
            other => other,
        }
    }
}

fn main() {
    let loan = LoanState::Requested { member: "alice".into() }.approve(1);
    let loan = loan.renew().unwrap_or_else(|(s, why)| { println!("{why}"); s });
    let loan = loan.tick(50);
    println!("{loan:?}");               // Overdue { member: "alice", days_late: 7 }
    println!("{:?}", loan.give_back()); // Returned { fee_cents: 350 }
}

Because each transition consumes self, a stale state cannot be used after a transition, and data such as due_day only exists in states where it is meaningful. For transitions that must be checked at compile time, see the typestate pattern on Generics & Const Generics.

#[non_exhaustive]

A library that may add variants (or fields) later can mark the enum (or struct) #[non_exhaustive]. Code outside the defining crate must then include a wildcard arm when matching, so adding a variant is not a breaking change. Inside the defining crate the attribute has no effect. std::io::ErrorKind is a well-known example.

#[non_exhaustive]
#[derive(Debug)]
pub enum CatalogEvent {
    BookAdded,
    BookLent,
}

fn log(e: &CatalogEvent) {
    match e {
        CatalogEvent::BookAdded => println!("added"),
        CatalogEvent::BookLent => println!("lent"),
        // In another crate, a `_ => ...` arm would be required here.
    }
}

fn main() {
    log(&CatalogEvent::BookLent);
    match std::io::ErrorKind::NotFound {
        std::io::ErrorKind::NotFound => println!("not found"),
        _ => println!("other"), // required: ErrorKind is #[non_exhaustive]
    }
}

Using variants

Variants are namespaced under their enum (Format::Ebook). Import them with use to write them bare — the standard library does this for Some/None/Ok/Err through the prelude. Inside an impl block, Self::Variant avoids repeating the enum name. Avoid glob-importing variants of large enums into wide scopes: a misspelt variant in a pattern then silently becomes a catch-all binding (the compiler warns, with lint bindings_with_variant_name).

#[derive(Debug)]
enum Format {
    Hardcover,
    Paperback,
}

fn price(f: &Format) -> u32 {
    use Format::{Hardcover, Paperback};
    match f {
        Hardcover => 2500,
        Paperback => 1200,
    }
}

fn main() {
    println!("{}", price(&Format::Hardcover));
}