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());
}
}
Source: The Book: Defining an Enum
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
}
Source: The Rust Reference: Enumerations
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));
}
Source: Error E0004 and The Book: Matches Are Exhaustive
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 |
|
Identifier |
anything; binds it to a name |
|
Wildcard |
anything; binds nothing |
|
Range |
a value within an inclusive or exclusive range (integers and |
|
Tuple |
a tuple, element by element |
|
Struct |
a struct or struct-like variant by field |
|
Tuple struct / variant |
positional fields |
|
Slice / array |
by length and position |
|
Rest |
the remaining elements or fields |
|
Or |
any of several patterns |
|
Binding with |
binds the value while also testing it |
|
Reference |
dereferences a reference |
|
Path |
a constant or unit variant |
|
Source: The Rust Reference: Patterns
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));
}
Source: The Book: Pattern Syntax
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}");
}
Source: The Book: Refutability
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.
#[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 }
}
Source: The Book: Defining an Enum
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));
}
Source: Rust by Example: use with enums
References
-
Rust by Example: match (destructuring, guards, binding)