Can you write this from memory?
Write a match expression on x that returns "one" for 1, "two" for 2, and "other" for anything else.
Pattern matching is one of Rust's most powerful features. The match expression lets you compare a value against patterns and run code based on which matches. It's exhaustive: you must handle every possibility.
Beyond match, you'll use if let for single patterns, while let for loops, let else for bind-or-return, and destructuring everywhere. Patterns work on tuples, structs, enums, slices, and references.
This page focuses on pattern syntax until matching becomes natural. From basic match arms to complex destructuring with guards and slice patterns.
The most common pattern matching error. Your match doesn't cover every variant:
// WRONG: missing None arm
fn describe(optional: Option<i32>) -> &'static str {
match optional {
Some(x) if x > 0 => "positive",
Some(_) => "non-positive",
// error[E0004]: non-exhaustive patterns: `None` not covered
}
}
// RIGHT: handle all cases
fn describe(optional: Option<i32>) -> &'static str {
match optional {
Some(x) if x > 0 => "positive",
Some(_) => "non-positive",
None => "nothing",
}
}
The compiler enforces exhaustiveness because unhandled cases are runtime bugs in other languages. In Rust, they're compile-time errors. If you add a variant to an enum, the compiler flags every match that needs updating. See structs and enums for how enum variants carry data that you destructure in match arms.
let bindings require irrefutable patterns — patterns that always match:
// WRONG: Some(x) might not match
// let Some(x) = some_option;
// error[E0005]: refutable pattern in local binding
// RIGHT Option 1: if let for "do something if it matches"
if let Some(x) = some_option {
println!("{x}");
}
// RIGHT Option 2: let else for "bind or return early"
let Some(x) = some_option else {
return; // must diverge: return, break, continue, or panic!
};
println!("{x}"); // x is in scope here
let else is the "bind-or-return" pattern — it unwraps or exits the current block. The else branch must diverge (it cannot fall through).
With | patterns, every alternative must bind the same names:
// WRONG: x is bound on the left but not the right
// match pair {
// (x, 0) | (0, _) => println!("{x}"), // error[E0408]
// _ => {}
// }
// RIGHT: bind x on both sides
match pair {
(x, 0) | (0, x) => println!("{x}"),
(a, b) => println!("({a}, {b})"),
}
Matching by value on borrowed data tries to move, which the borrow checker rejects:
// WRONG: matching by value moves out of the reference
// fn first_name(name: &Option<String>) -> String {
// match name {
// Some(s) => s.clone(), // error[E0507]: cannot move out of *name
// None => String::new(),
// }
// }
// RIGHT: match on the reference — bind by reference
fn first_name(name: &Option<String>) -> String {
match name {
Some(s) => s.clone(), // s is &String here (match ergonomics)
None => String::new(),
}
}
// Or be explicit:
fn first_name_explicit(name: &Option<String>) -> String {
match *name {
Some(ref s) => s.clone(), // ref avoids the move
None => String::new(),
}
}
When matching on a reference, Rust automatically adjusts binding modes so Some(s) binds s as a reference. This is called match ergonomics. For a copy-ready reference of all pattern syntax, see the pattern matching cheat sheet.
Pull values out of structs, tuples, and enums directly in the pattern:
// Struct destructuring
let Point { x, y } = point;
// Tuple destructuring (use _ to ignore)
let (first, second, _) = tuple;
// Enum destructuring in match
match msg {
Message::Move { x, y } => println!("move to ({x}, {y})"),
Message::Write(text) => println!("{text}"),
Message::Quit => println!("quit"),
}
// Nested destructuring
let ((a, b), Point { x, .. }) = (pair, point);
Patterns are not limited to match:
// if let: handle one pattern, skip the rest
if let Some(value) = optional {
println!("{value}");
}
// while let: loop until the pattern stops matching
while let Some(value) = stack.pop() {
println!("{value}");
}
// let else: bind or return early (must diverge)
let Ok(port) = env::var("PORT")
.map(|v| v.parse::<u16>())
.unwrap_or(Ok(8080)) else {
eprintln!("invalid PORT");
std::process::exit(1);
};
Patterns appear in more places than just match and if let:
// let bindings
let (x, y) = (1, 2);
// Function parameters
fn print_point(&Point { x, y }: &Point) {
println!("({x}, {y})");
}
// for loops
for (i, value) in vec.iter().enumerate() {
println!("{i}: {value}");
}
// Closure parameters
let sum = |(a, b): (i32, i32)| a + b;
Guards
Add conditions after a pattern with if. Guards don't affect exhaustiveness checking — the compiler still requires the patterns themselves to cover all possibilities:
match temperature {
t if t > 100 => "boiling",
t if t < 0 => "freezing",
_ => "normal", // required: guards don't count as exhaustive
}
Precedence gotcha: a guard applies to the entire | pattern, not just the last alternative:
match x {
// guard applies to BOTH 4 and 5, not just 5
4 | 5 if condition => "matched",
_ => "other",
}
@ Bindings
Bind a value to a name while simultaneously testing it against a pattern:
match msg {
Message::Hello { id: id @ 3..=7 } => println!("id in range: {id}"),
Message::Hello { id } => println!("other id: {id}"),
_ => (),
}
// @ with ranges
match age {
n @ 0..=12 => println!("child aged {n}"),
n @ 13..=19 => println!("teenager aged {n}"),
n => println!("adult aged {n}"),
}
Match multiple patterns in a single arm:
match x {
1 | 2 | 3 => println!("small"),
_ => println!("big"),
}
Every alternative in a | pattern must bind the same variable names (E0408).
Match on slices with [] patterns. Use .. for the rest (only once per pattern):
match slice {
[] => println!("empty"),
[one] => println!("single: {one}"),
[first, .., last] => println!("first: {first}, last: {last}"),
}
// Bind the tail with @
match slice {
[head, tail @ ..] => println!("head: {head}, tail has {} elements", tail.len()),
[] => println!("empty"),
}
// Bind the whole slice and destructure
match slice {
whole @ [.., last] => println!("last of {}: {last}", whole.len()),
[] => println!("empty"),
}
Only one .. is allowed per pattern — two would be ambiguous about which elements to skip. Exhaustive pattern matching on Result and Option is central to Rust's error handling approach, where the ? operator provides a concise alternative to match for propagating errors. Pattern matching also enables the Strategy design pattern, where different match arms dispatch to different behaviors.
- The Rust Book: Patterns and Matching — exhaustive treatment of all pattern types
- The Rust Book: Pattern Syntax — guards, @, |, .., and nested patterns
- Rust Reference: Patterns — formal grammar, all pattern kinds
- Rust By Example: match — practical examples with destructuring
When to Use Rust Pattern Matching: match, if let, Destructuring
- Use `match` when you need to handle multiple cases exhaustively.
- Use `if let` when you only care about one pattern and want to ignore the rest.
- Use `let else` for bind-or-return-early patterns.
- Use `while let` to loop while a pattern matches.
- Use destructuring to extract data from complex types.
- Use guards (`if condition`) for additional constraints on match arms.
Check Your Understanding: Rust Pattern Matching: match, if let, Destructuring
Why is match exhaustive and why does that matter?
Match forces you to handle every possible value of the type. This prevents bugs from forgotten cases. If you add a variant to an enum, the compiler shows every match that needs updating.
What You'll Practice: Rust Pattern Matching: match, if let, Destructuring
Common Rust Pattern Matching: match, if let, Destructuring Pitfalls
- You get `error[E0004]: non-exhaustive patterns` -- you forgot a variant in your `match`. Add the missing arm or use `_ => ...` as a catch-all. Adding a variant to an enum will flag every incomplete match.
- You get `error[E0005]: refutable pattern in local binding` -- you used `let Some(x) = ...` but the value might be `None`. Switch to `if let Some(x) = ...` or `let Some(x) = ... else { return; };`.
- You get `error[E0408]: variable is not bound in all patterns` -- you wrote `A(x) | B` with `|` but only one side binds `x`. Every alternative must bind the same variable names.
- You get `error[E0507]: cannot move out of borrowed content` -- matching by value on borrowed data tries to move. Match on the reference directly (match ergonomics) or use `ref` to bind by reference.
- Your catch-all arm `_` runs when you expected a specific arm to match -- arms are checked top to bottom. A broad pattern above shadows specific ones below. Move specific patterns first.
- A guard on a `|` pattern behaves unexpectedly -- the guard applies to the entire `|` expression, not just the last alternative. `4 | 5 if cond` means `(4 | 5) if cond`, not `4 | (5 if cond)`.
Rust Pattern Matching: match, if let, Destructuring FAQ
What is the difference between match and if let?
`match` is exhaustive and you handle every case. `if let` handles one pattern and ignores the rest. Use `if let` when you only care about one variant: `if let Some(x) = option { }`.
How do I match multiple patterns in one arm?
Use the `|` operator: `1 | 2 | 3 => println!("one to three")`. This matches any of the listed patterns.
What are match guards?
Extra conditions on match arms: `Some(x) if x > 5 => ...`. The arm only matches if both the pattern and guard are true.
How do I ignore parts of a pattern?
Use `_` for values you do not need: `(x, _)` ignores the second element. Use `..` to ignore multiple: `Point { x, .. }` ignores y and z.
Can I bind a value and match a pattern at the same time?
Use `@` for binding: `Some(x @ 1..=5) => println!("got {x}")`. This both matches the range and binds the value to `x`.
What is let else?
`let PATTERN = expr else { diverge };` binds if the pattern matches, otherwise executes the else block which must diverge (return, break, continue, or panic). It replaces the common "match or return" pattern.
What is match ergonomics?
When matching on a reference, Rust automatically adjusts binding modes so `Some(x)` binds `x` as a reference instead of trying to move. This avoids needing `ref` in most cases.
Rust Pattern Matching: match, if let, Destructuring Syntax Quick Reference
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("other"),
}match optional {
Some(value) => println!("got {value}"),
None => println!("nothing"),
}match x {
1 | 2 | 3 => println!("small"),
_ => println!("big"),
}match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}match x {
Some(n) if n > 5 => println!("big"),
Some(n) => println!("small: {n}"),
None => println!("none"),
}if let Some(value) = optional {
println!("got {value}");
}while let Some(value) = stack.pop() {
println!("{value}");
}let Some(x) = optional else {
return;
};match slice {
[first, .., last] => println!("{first} {last}"),
[] => println!("empty"),
}let Point { x, y } = point;let (first, second, _) = tuple;match msg {
Message::Hello { id: id @ 3..=7 } => println!("id in range: {id}"),
_ => (),
}Rust Pattern Matching: match, if let, Destructuring Sample Exercises
Fill in the match arm to return "zero" when n is 0.
"zero"Fill in the keyword to start a match expression.
matchFill in the pattern to handle the empty case.
None+ 49 more exercises
Copy-ready syntax examples for quick lookup