TL;DR: Pattern matching in Rust lets you branch on the shape and content of values, not just their equality. The
matchexpression is exhaustive: the compiler requires you to handle every possible case, preventing missed branches. Patterns can destructure structs, enums, tuples, and slices; bind variables; add guard conditions; and nest arbitrarily.if letandwhile letare shorthand for matching a single pattern. Pattern matching is central to usingOption<T>,Result<T, E>, and enums in idiomatic Rust.
What Is Pattern Matching in Rust?
A match expression tests a value against a series of patterns and executes the first matching arm, the compiler guarantees every possible value is covered.
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1..=9 => "single digit",
10..=99 => "double digits",
100 => "exactly one hundred",
n if n < 0 => "negative", // guard condition
_ => "large", // wildcard; matches anything
}
}
fn main() {
println!("{}", describe(7)); // "single digit"
println!("{}", describe(-5)); // "negative"
println!("{}", describe(500)); // "large"
}Unlike switch in C/Java, Rust's match does not fall through, requires exhaustive coverage, and can match on complex patterns including ranges, tuples, and bound variables.
How Do You Destructure Enums With match?
Pattern matching on enums is the primary way to extract data from variants, binding inner values to variables for use in each arm.
#[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
// Matching Option<T>
fn double(x: Option<i32>) -> Option<i32> {
match x {
Some(n) => Some(n * 2),
None => None,
}
}
// Matching Result<T, E>
fn parse_and_double(s: &str) -> Result<i32, String> {
match s.parse::<i32>() {
Ok(n) => Ok(n * 2),
Err(e) => Err(format!("Parse error: {e}")),
}
}If you add a new variant to Shape without updating the match, the compiler produces an error, you cannot forget a case.
What Is if let and When Should You Use It?
if let matches a single pattern, ignoring all other cases, shorthand for match when you only care about one variant.
fn main() {
let config: Option<String> = Some("debug".to_string());
// Verbose match:
match &config {
Some(level) => println!("Log level: {level}"),
None => {} // explicitly doing nothing
}
// Equivalent if let; more readable when only one arm matters:
if let Some(level) = &config {
println!("Log level: {level}");
}
// if let with else
if let Some(level) = &config {
println!("Log level: {level}");
} else {
println!("No log level configured");
}
// while let; loop until pattern stops matching
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}");
}
}How Does Destructuring Work?
Destructuring lets patterns pull apart structs, tuples, slices, and nested types, binding their contents to named variables in one step.
struct Point { x: f64, y: f64 }
fn main() {
// Struct destructuring
let Point { x, y } = Point { x: 3.0, y: 4.0 };
println!("x={x}, y={y}");
// Tuple destructuring
let (a, b, c) = (1, "hello", 3.14);
// Nested destructuring
let ((feet, inches), name) = ((5, 11), "Alice");
// Slice patterns
let numbers = [1, 2, 3, 4, 5];
match numbers {
[first, .., last] => println!("first={first}, last={last}"),
}
// Ignore fields with ..
let Point { x, .. } = Point { x: 1.0, y: 2.0 };
// Bind and also test with @
let n = 7;
match n {
x @ 1..=10 => println!("{x} is between 1 and 10"),
_ => println!("out of range"),
}
}How Is Rust Pattern Matching Different From Switch Statements?
Rust match is far more powerful, it destructures, binds variables, supports guards, and is exhaustiveness-checked at compile time.
| Feature | C/Java switch | Rust match |
|---|---|---|
| Falls through by default | ✅ (bug-prone) | ❌ (never) |
| Exhaustiveness check | ❌ | ✅ Compile error if missing |
| Destructure data | ❌ | ✅ |
| Bind inner values | ❌ | ✅ |
| Guard conditions | ❌ | ✅ if guard |
| Match ranges | ❌ (Java) / partial (C) | ✅ 1..=9 |
| Match multiple patterns | ❌ | ✅ 1 | 2 | 3 |
Frequently Asked Questions
_ is the wildcard pattern, it matches any value and does not bind it. Use it as the final arm in match to handle all remaining cases. _x binds to a variable named _x (suppresses unused variable warnings). .. ignores remaining fields in a struct or elements in a tuple.
Yes. match &value { &Some(ref x) => ... } or simply match value { Some(x) => ... } ; Rust performs automatic match ergonomics, adding & and ref where needed so you can usually write the intuitive pattern.
A match guard is an extra condition added to an arm: n if n > 0 => .... The arm only matches if both the pattern matches and the guard is true. Guards cannot be used for exhaustiveness, the compiler treats arms with guards as potentially not matching.
Yes. let uses pattern matching too: let (x, y) = (1, 2); destructures a tuple. let Ok(value) = result else { return; }; is let-else, it binds the pattern or executes the else block (which must diverge with return, break, or panic!).
Sources
- The Rust Book ; The
matchControl Flow Construct - The Rust Book ; Patterns and Matching
- Rust Reference ; Patterns
Related Glossary Terms
- Enum: Enums are the primary target of pattern matching
- Option:
Some(x)/Nonepatterns are used constantly - Result:
Ok(x)/Err(e)patterns for error handling - Struct: Structs can be destructured in patterns
- if let:
if letis the shortest path from full match syntax to common real-world code
