Pattern Matching
Pattern matching is one of Rust’s most powerful features. The compiler guarantees exhaustive handling of all cases, making it impossible to forget an edge case.
Match Expressions
match is like a switch statement, but it’s an expression (returns a value) and the compiler enforces exhaustiveness.
enum Direction {
North,
South,
East,
West,
}
fn describe(dir: Direction) -> &'static str {
match dir {
Direction::North => "heading up",
Direction::South => "heading down",
Direction::East => "heading right",
Direction::West => "heading left",
}
// If you remove a variant, this won't compile.
// The compiler forces you to handle every case.
}
Pattern Types
Literal patterns
let x = 42;
match x {
0 => println!("zero"),
1..=9 => println!("single digit"),
10 | 20 | 30 => println!("round number"),
42 => println!("the answer"),
_ => println!("something else"), // _ is a wildcard
}
Destructuring structs and tuples
struct Point {
x: f64,
y: f64,
}
let p = Point { x: 0.0, y: 5.0 };
match p {
Point { x: 0.0, y } => println!("on y-axis at y={y}"),
Point { x, y: 0.0 } => println!("on x-axis at x={x}"),
Point { x, y } => println!("at ({x}, {y})"),
}
// Tuples
let pair = (true, 42);
match pair {
(true, n) if n > 0 => println!("positive truth: {n}"),
(false, _) => println!("falsy"),
_ => println!("other"),
}
Destructuring enums with data
enum Command {
Quit,
Echo(String),
Move { x: i32, y: i32 },
Color(u8, u8, u8),
}
fn handle(cmd: Command) {
match cmd {
Command::Quit => println!("quitting"),
Command::Echo(msg) => println!("{msg}"),
Command::Move { x, y } => println!("moving to ({x}, {y})"),
Command::Color(r, g, b) => println!("color: #{r:02x}{g:02x}{b:02x}"),
}
}
Guards
Add conditions to patterns with if:
let num = Some(42);
match num {
Some(n) if n < 0 => println!("negative: {n}"),
Some(n) if n > 100 => println!("large: {n}"),
Some(n) => println!("normal: {n}"),
None => println!("nothing"),
}
Gotcha: Guards don’t count toward exhaustiveness. The compiler can’t verify arbitrary boolean conditions, so you still need a catch-all pattern after guarded arms.
Bindings with @
Bind a value to a name while also testing it against a pattern:
match status_code {
code @ 200..=299 => println!("success: {code}"),
code @ 400..=499 => println!("client error: {code}"),
code @ 500..=599 => println!("server error: {code}"),
other => println!("unexpected: {other}"),
}
Nested patterns
Patterns compose deeply:
let msg: Option<Result<String, io::Error>> = Some(Ok("hello".into()));
match msg {
Some(Ok(text)) => println!("got: {text}"),
Some(Err(e)) => println!("error: {e}"),
None => println!("nothing"),
}
Option and Result Matching
The most common use of pattern matching:
fn find_user(id: u32) -> Option<String> {
if id == 1 { Some("Alice".into()) } else { None }
}
// Full match
match find_user(1) {
Some(name) => println!("Found: {name}"),
None => println!("Not found"),
}
// Result matching with error handling
match std::fs::read_to_string("config.toml") {
Ok(content) => process_config(&content),
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
println!("No config file, using defaults");
use_defaults();
}
Err(e) => return Err(e.into()),
}
if let and while let
When you only care about one variant:
// Instead of matching all cases
if let Some(name) = find_user(1) {
println!("Found: {name}");
}
// Process items from a channel/iterator
while let Some(msg) = rx.recv().await {
println!("Got: {msg}");
}
// With else
if let Ok(port) = env::var("PORT") {
println!("Using port {port}");
} else {
println!("Using default port 8080");
}
Let Chains (Rust 1.88+)
Chain multiple let patterns and conditions together. Replaces deeply nested if let blocks.
// Before: nested if let
if let Some(user) = get_user(id) {
if let Some(email) = user.email {
if email.ends_with("@company.com") {
send_internal_notification(&email);
}
}
}
// After: let chains
if let Some(user) = get_user(id)
&& let Some(email) = user.email
&& email.ends_with("@company.com")
{
send_internal_notification(&email);
}
Tip: Let chains work in
while lettoo. They flatten what used to be deeply nested conditional extraction into readable linear code.
Enum-Based State Machines
Pattern matching shines for state machines. The compiler ensures every state transition is handled.
enum ConnectionState {
Disconnected,
Connecting { attempt: u32, max_attempts: u32 },
Connected { session_id: String },
Error { message: String, retryable: bool },
}
fn next_state(state: ConnectionState) -> ConnectionState {
match state {
ConnectionState::Disconnected => {
ConnectionState::Connecting { attempt: 1, max_attempts: 5 }
}
ConnectionState::Connecting { attempt, max_attempts } if attempt >= max_attempts => {
ConnectionState::Error {
message: format!("failed after {max_attempts} attempts"),
retryable: false,
}
}
ConnectionState::Connecting { attempt, max_attempts } => {
// Try to connect...
match try_connect() {
Ok(id) => ConnectionState::Connected { session_id: id },
Err(_) => ConnectionState::Connecting {
attempt: attempt + 1,
max_attempts,
},
}
}
ConnectionState::Connected { session_id } => {
println!("Active session: {session_id}");
ConnectionState::Connected { session_id }
}
ConnectionState::Error { retryable: true, .. } => {
ConnectionState::Disconnected // retry from start
}
ConnectionState::Error { message, .. } => {
panic!("Unrecoverable error: {message}");
}
}
}
Adding a new variant to ConnectionState forces you to update every match that handles it. The compiler won’t let you forget.
matches! Macro
Quick boolean check against a pattern:
let value = Some(42);
// Instead of: if let Some(_) = value { true } else { false }
let has_value = matches!(value, Some(_));
// With guards
let is_positive = matches!(value, Some(n) if n > 0);
// Multiple patterns
let is_vowel = matches!(c, 'a' | 'e' | 'i' | 'o' | 'u');
Patterns Everywhere
Patterns aren’t just for match. They work in:
// Function parameters
fn print_point(&(x, y): &(f64, f64)) {
println!("({x}, {y})");
}
// let bindings
let (first, second) = ("hello", "world");
let Point { x, y } = some_point;
// for loops
for (index, value) in vec.iter().enumerate() {
println!("{index}: {value}");
}
// Closures
let sum: i32 = pairs.iter().map(|(a, b)| a + b).sum();
Next: Lifetimes | Async Rust