Rust’s match expression is more powerful than switch statements in most languages. It’s exhaustive (compiler-verified), expression-based (returns a value), and handles destructuring, guards, binding, and ranges in a unified syntax. Mastering match is key to idiomatic Rust.
Basic Syntax
match compares a value against a series of patterns. The first matching pattern executes its arm. Every arm must return the same type (or ! for diverging):
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1 => "one",
2 | 3 => "two or three", // multiple values
4..=9 => "four through nine", // inclusive range
10..=99 => "two digits",
_ => "large or negative", // catch-all
}
}
fn main() {
println!("{}", describe(0)); // zero
println!("{}", describe(3)); // two or three
println!("{}", describe(7)); // four through nine
println!("{}", describe(100)); // large or negative
}
match is an expression — it returns the value of the matching arm:
let grade = match score {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
60..=69 => 'D',
_ => 'F',
};
Exhaustiveness
The compiler requires all possible values to be covered. Missing a case is a compile error:
enum Color { Red, Green, Blue }
let color = Color::Red;
// This doesn't compile — Blue is not handled:
// match color {
// Color::Red => println!("red"),
// Color::Green => println!("green"),
// }
// This compiles:
match color {
Color::Red => println!("red"),
Color::Green => println!("green"),
Color::Blue => println!("blue"),
}
This is the key difference from switch statements. You can’t forget a case.
Destructuring Enums
The most powerful use of match — extracting data from enum variants:
#[derive(Debug)]
enum HttpResponse {
Ok(String),
Created { id: u64, location: String },
BadRequest(Vec<String>),
NotFound,
InternalError { code: u32, message: String },
}
fn handle(response: HttpResponse) {
match response {
HttpResponse::Ok(body) => {
println!("200 OK: {}", body);
}
HttpResponse::Created { id, location } => {
println!("201 Created: id={}, location={}", id, location);
}
HttpResponse::BadRequest(errors) => {
for e in &errors {
println!("400 Bad Request: {}", e);
}
}
HttpResponse::NotFound => {
println!("404 Not Found");
}
HttpResponse::InternalError { code, message } => {
eprintln!("500 Error {}: {}", code, message);
}
}
}
Destructuring Structs
Match can destructure struct fields directly:
#[derive(Debug)]
struct Point { x: i32, y: i32 }
fn classify_point(p: Point) -> &'static str {
match p {
Point { x: 0, y: 0 } => "origin",
Point { x, y: 0 } => "on x-axis",
Point { x: 0, y } => "on y-axis",
Point { x, y } if x == y => "on diagonal",
Point { x, y } if x == -y => "on anti-diagonal",
_ => "somewhere else",
}
}
fn main() {
println!("{}", classify_point(Point { x: 0, y: 0 })); // origin
println!("{}", classify_point(Point { x: 5, y: 0 })); // on x-axis
println!("{}", classify_point(Point { x: 3, y: 3 })); // on diagonal
}
Rename fields in destructuring with field: new_name:
struct Config { host: String, port: u16, debug: bool }
let cfg = Config { host: "localhost".to_string(), port: 8080, debug: true };
let Config { host: h, port: p, debug: _ } = cfg;
println!("{}:{}", h, p); // localhost:8080
Destructuring Tuples
fn main() {
let pair = (0, -2);
let description = match pair {
(0, y) => format!("on y-axis at {}", y),
(x, 0) => format!("on x-axis at {}", x),
(x, y) if x == y => format!("on diagonal at {}", x),
(x, y) => format!("at ({}, {})", x, y),
};
println!("{}", description);
// Tuple destructuring with ..
let (a, b, .., z) = (1, 2, 3, 4, 5);
println!("first={}, second={}, last={}", a, b, z);
}
Destructuring Slices
Match on the shape of a slice — very useful for recursive algorithms and parsing:
fn describe_list(v: &[i32]) -> String {
match v {
[] => "empty".to_string(),
[x] => format!("single: {}", x),
[x, y] => format!("pair: {} and {}", x, y),
[first, .., last] => format!("starts with {}, ends with {}", first, last),
}
}
fn main() {
println!("{}", describe_list(&[])); // empty
println!("{}", describe_list(&[42])); // single: 42
println!("{}", describe_list(&[1, 2])); // pair: 1 and 2
println!("{}", describe_list(&[1, 2, 3, 4])); // starts with 1, ends with 4
}
Recursive list processing with slice patterns:
fn sum(v: &[i32]) -> i32 {
match v {
[] => 0,
[head, tail @ ..] => head + sum(tail),
}
}
fn main() {
println!("{}", sum(&[1, 2, 3, 4, 5])); // 15
}
Match Guards
An if condition after the pattern provides extra filtering:
fn main() {
let num = Some(7);
let description = match num {
Some(x) if x < 0 => format!("negative: {}", x),
Some(x) if x == 0 => "zero".to_string(),
Some(x) if x % 2 == 0 => format!("positive even: {}", x),
Some(x) => format!("positive odd: {}", x),
None => "none".to_string(),
};
println!("{}", description); // positive odd: 7
// Guard with | (applies to all alternatives)
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"), // prints this
}
// The guard `if y` applies to the whole `4 | 5 | 6`
}
@ Bindings — Capture and Test Simultaneously
The @ operator creates a variable binding while also testing the value against a pattern:
fn classify_age(age: u32) -> &'static str {
match age {
n @ 0..=12 => "child",
n @ 13..=17 => "teenager",
n @ 18..=64 => "adult",
n @ 65..=u32::MAX => "senior",
_ => unreachable!(),
}
}
// More useful: capture AND use the value in the arm
fn validate_id(id: u64) {
match id {
n @ 1..=999 => println!("Low ID: {}", n),
n @ 1000..=9999 => println!("Medium ID: {}", n),
n => println!("High ID: {}", n),
}
}
// @ with destructuring
#[derive(Debug)]
enum Message {
Hello { id: i32 },
}
fn main() {
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_var @ 1..=10 } => {
println!("Small id: {}", id_var)
}
Message::Hello { id } => {
println!("Other id: {}", id)
}
}
}
Ignoring Values
fn main() {
let numbers = (2, 4, 8, 16, 32);
// _ ignores one value
match numbers {
(first, _, third, _, fifth) => {
println!("{}, {}, {}", first, third, fifth); // 2, 8, 32
}
}
// .. ignores remaining fields
struct Point3D { x: i32, y: i32, z: i32 }
let p = Point3D { x: 1, y: 2, z: 3 };
match p {
Point3D { x, .. } => println!("x = {}", x), // 1
}
// .. in tuple — ignore middle
match numbers {
(first, .., last) => println!("{} → {}", first, last), // 2 → 32
}
// Underscore prefix to suppress "unused variable" warning
let _unused = String::from("doesn't matter");
// _unused is bound but not used — no warning
// _ alone is NOT bound — it's a wildcard, doesn't own the value
}
Nested Patterns
Match can destructure deeply nested structures:
#[derive(Debug)]
enum Shape {
Circle { center: (f64, f64), radius: f64 },
Rectangle { top_left: (f64, f64), bottom_right: (f64, f64) },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius, .. } => std::f64::consts::PI * radius * radius,
Shape::Rectangle {
top_left: (x1, y1),
bottom_right: (x2, y2),
} => (x2 - x1).abs() * (y2 - y1).abs(),
}
}
fn main() {
let shapes = vec![
Shape::Circle { center: (0.0, 0.0), radius: 5.0 },
Shape::Rectangle { top_left: (0.0, 10.0), bottom_right: (4.0, 0.0) },
];
for s in &shapes {
println!("{:?} → area = {:.2}", s, area(s));
}
}
if let and while let
Concise single-pattern matching:
fn main() {
// if let — match one variant
let config = Some(3u8);
if let Some(max) = config {
println!("Max: {}", max);
}
// if let with else
if let Some(max) = config {
println!("Configured: {}", max);
} else {
println!("Using default");
}
// Chained if let else if
let value: Result<i32, &str> = Ok(42);
if let Ok(n) = value {
println!("Ok: {}", n);
} else if let Err(e) = value {
println!("Err: {}", e);
}
// while let — drain a stack
let mut stack = vec!["first", "second", "third"];
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
}
let else — Bind or Diverge
Rust 1.65+. If the pattern doesn’t match, the else block must diverge (return, break, continue, or panic):
fn process(input: &str) -> Result<u32, String> {
let Ok(n) = input.trim().parse::<u32>() else {
return Err(format!("'{}' is not a valid number", input));
};
// n is bound and available here
Ok(n * 2)
}
fn main() {
println!("{:?}", process("21")); // Ok(42)
println!("{:?}", process("abc")); // Err("'abc' is not a valid number")
println!("{:?}", process(" 99 ")); // Ok(198)
}
let else is cleaner than the match { return Err... } pattern for validation.
The matches! Macro
Returns a boolean: true if the value matches the pattern:
#[derive(Debug)]
enum Status { Active, Inactive, Banned }
fn main() {
let s = Status::Active;
// Verbose:
let is_usable = match s {
Status::Active => true,
_ => false,
};
// Concise:
let is_usable = matches!(s, Status::Active);
println!("{}", is_usable); // true
// With guard
let n = Some(42i32);
println!("{}", matches!(n, Some(x) if x > 0)); // true
// Multiple patterns
println!("{}", matches!(s, Status::Active | Status::Inactive)); // true
// Use in filter
let statuses = vec![Status::Active, Status::Banned, Status::Active, Status::Inactive];
let active_count = statuses.iter().filter(|s| matches!(s, Status::Active)).count();
println!("Active: {}", active_count); // 2
}
Real-World Patterns
State Machine Transitions
#[derive(Debug, Clone, PartialEq)]
enum State {
Idle,
Running { job_id: u64, progress: f32 },
Paused { job_id: u64, progress: f32 },
Complete { job_id: u64 },
Failed { job_id: u64, error: String },
}
impl State {
fn transition(self, event: &str) -> State {
match (self, event) {
(State::Idle, "start") => State::Running { job_id: 1, progress: 0.0 },
(State::Running { job_id, progress }, "pause") => State::Paused { job_id, progress },
(State::Paused { job_id, progress }, "resume") => State::Running { job_id, progress },
(State::Running { job_id, .. }, "complete") => State::Complete { job_id },
(State::Running { job_id, .. }, "fail") => State::Failed {
job_id,
error: "unexpected failure".to_string(),
},
(state, event) => {
println!("Invalid: {:?} + {}", state, event);
state
}
}
}
}
Command Dispatch
#[derive(Debug)]
enum Command {
Get { key: String },
Set { key: String, value: String, ttl: Option<u64> },
Delete { key: String },
Flush,
}
fn dispatch(cmd: Command) -> String {
match cmd {
Command::Get { key } => format!("GET {}", key),
Command::Set { key, value, ttl: None } => {
format!("SET {} = {}", key, value)
}
Command::Set { key, value, ttl: Some(ttl) } => {
format!("SET {} = {} EX {}", key, value, ttl)
}
Command::Delete { key } => format!("DEL {}", key),
Command::Flush => "FLUSHALL".to_string(),
}
}
Summary
| Feature | Syntax | Use when |
|---|---|---|
| Literal match | 1 | 2 | 3 |
Fixed values |
| Range | 1..=10 |
Ranges of integers/chars |
| Variable binding | n |
Capture any value |
| Wildcard | _ |
Ignore a value |
| Guard | x if x > 0 |
Extra conditions |
@ binding |
n @ 1..=10 |
Capture + test |
| Destructure enum | Variant { field } |
Extract enum data |
| Destructure struct | Struct { x, y } |
Extract struct fields |
| Destructure tuple | (a, b, c) |
Extract tuple elements |
| Slice pattern | [first, .., last] |
Match slice shape |
if let |
if let Some(x) = opt |
Single-pattern match |
let else |
let Ok(x) = r else { return; } |
Bind or diverge |
matches! |
matches!(x, Pattern) |
Boolean pattern test |
Resources
- Rust Book: The
matchControl Flow Construct - Rust Book: Patterns and Matching
- Rust Reference: Pattern syntax
Comments