When one match isn't enough
You are parsing a configuration file. The parser returns a Result<Config, Error>. Inside Config, there is a Server struct with host and port. You need the host. Without nested patterns, you write a match on the result, then inside the success branch you write another match on the config, then inside that you extract the server. Three levels of indentation. Three levels of mental tracking.
Nested patterns let you smash those layers together. You match the result, the config, and the server fields in a single expression. The code stays flat. The logic stays clear.
Patterns as templates
Think of a pattern as a template for data. When you match, you are asking the compiler: "Does this value fit this template? If so, hand me the pieces."
Nested patterns extend that idea. You can describe a template that has holes inside holes. The outer template checks the enum variant. The inner template checks the struct fields or tuple elements. The compiler verifies the whole shape at once and binds all the variables in one step.
You do not open the outer box, close it, and then open the inner box. You reach through the layers and grab what you need while verifying the structure.
Minimal example
This example shows how to destructure enums with data using nested patterns. The code binds values directly from the inner structure.
/// Represents different types of messages in a system.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
/// Processes a message using nested pattern matching.
fn process_message(msg: Message) {
match msg {
// Destructure the struct variant directly.
// The compiler checks the variant and binds x and y in one step.
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
// Match the tuple variant and bind all three components.
// Order matters: r gets the first i32, g the second, b the third.
Message::ChangeColor(r, g, b) => println!("RGB: {}, {}, {}", r, g, b),
// Match the variant but ignore the inner String.
// The underscore tells the compiler you do not need the value.
Message::Write(_) => println!("Ignoring text message"),
// Match a variant with no data.
Message::Quit => println!("Shutting down"),
}
}
fn main() {
process_message(Message::Move { x: 10, y: 20 });
process_message(Message::ChangeColor(255, 0, 0));
process_message(Message::Write("Hello".to_string()));
process_message(Message::Quit);
}
Patterns describe the shape of your data. Match the shape, bind the values, move on.
How the compiler handles nesting
When you write Message::Move { x, y }, the compiler generates code that checks the discriminant of the enum. If it matches Move, the compiler calculates the offset to the x and y fields and binds them to local variables.
Nested patterns do not add runtime overhead. The compiler optimizes a nested match identically to sequential matches. The difference is purely in your source code. Nested patterns keep the indentation shallow and the variable bindings close to where they are used.
The compiler also checks exhaustiveness across the whole nested structure. If you match Message::Move { x, y } but forget Message::Quit, the compiler rejects the code. It ensures every possible shape is handled, regardless of how many layers deep the pattern goes.
Real-world nesting: Result and Option
The most common nesting in Rust involves Result and Option. A database query might return Result<Option<User>, DbError>. The result can be an error or success. The success can be a user or nothing.
Nested patterns let you handle all four cases in a flat structure.
/// Represents a user in the database.
#[derive(Debug)]
struct User {
id: u64,
name: String,
}
/// Represents database errors.
#[derive(Debug)]
enum DbError {
ConnectionFailed,
QueryFailed(String),
}
/// Handles a database query result using nested patterns.
fn handle_query(result: Result<Option<User>, DbError>) {
match result {
// Success path: Result is Ok, Option is Some.
// Bind the user directly from the nested layers.
Ok(Some(user)) => println!("Found user: {:?}", user),
// Success path: Result is Ok, Option is None.
// The user does not exist, but the query succeeded.
Ok(None) => println!("User not found"),
// Error path: Result is Err, variant is ConnectionFailed.
Err(DbError::ConnectionFailed) => println!("Database is down"),
// Error path: Result is Err, variant is QueryFailed.
// Bind the error message string.
Err(DbError::QueryFailed(msg)) => println!("Query error: {}", msg),
}
}
fn main() {
handle_query(Ok(Some(User { id: 1, name: "Alice".to_string() })));
handle_query(Ok(None));
handle_query(Err(DbError::ConnectionFailed));
handle_query(Err(DbError::QueryFailed("Timeout".to_string())));
}
Convention aside: Ok(Some(user)) is the idiomatic way to write this. Do not write match result { Ok(opt) => match opt { Some(user) => ... } }. The nested form is standard in Rust codebases. It signals that you are inspecting multiple layers intentionally.
Flatten your matches. The compiler tracks the nesting for you.
Guards: Adding logic to patterns
Sometimes the shape is not enough. You need to check a value. Patterns match structure. Guards match values.
A guard is an if clause attached to a pattern. The guard runs only if the pattern matches. This saves work. If the variant is wrong, the guard never executes.
/// Checks a number using patterns and guards.
fn check_number(n: i32) {
match n {
// Pattern matches any i32.
// Guard checks the value.
n if n > 0 => println!("Positive"),
n if n < 0 => println!("Negative"),
_ => println!("Zero"),
}
}
fn main() {
check_number(5);
check_number(-3);
check_number(0);
}
Guards can also use nested patterns. You can match a structure and then check a field value.
/// Processes a move with a guard on coordinates.
fn process_move(msg: Message) {
match msg {
// Match the Move variant.
// Guard checks if x is zero.
Message::Move { x, y } if x == 0 => println!("Vertical move to y={}", y),
// Match the Move variant.
// Guard checks if y is zero.
Message::Move { x, y } if y == 0 => println!("Horizontal move to x={}", x),
// Fallback for other moves.
Message::Move { x, y } => println!("Diagonal move to ({}, {})", x, y),
_ => println!("Not a move"),
}
}
Use guards for value checks, patterns for structure checks. Keep the separation clean.
Or patterns: Merging branches
Multiple variants might share the same logic. Or patterns let you merge them with the | operator.
The branches must bind the same variables. You cannot bind x in one arm and y in the other. The compiler requires the bindings to align.
/// Represents keyboard keys.
enum Key {
Up,
Down,
Left,
Right,
Escape,
Space,
}
/// Handles key presses using or patterns.
fn handle_key(key: Key) {
match key {
// Merge multiple variants that share logic.
// All arms must bind the same variables (none here).
Key::Up | Key::Down | Key::Left | Key::Right => {
println!("Directional input")
}
Key::Escape => println!("Quit"),
Key::Space => println!("Action"),
}
}
fn main() {
handle_key(Key::Up);
handle_key(Key::Escape);
}
Or patterns work with nested data too. You can merge variants that have the same inner structure.
/// Represents shapes with dimensions.
enum Shape {
Circle { radius: f64 },
Square { side: f64 },
}
/// Calculates area using or patterns on nested fields.
fn get_area(shape: Shape) -> f64 {
match shape {
// Both variants have a single f64 field.
// Bind it to `size` in both cases.
Shape::Circle { radius: size } | Shape::Square { side: size } => {
println!("Using size {}", size);
size * size // Simplified for demo
}
}
}
Merge branches with |. Less code, same logic.
The @ binding: Check and keep
Sometimes you need to check part of a value but keep the whole thing. The @ operator lets you bind the entire value while matching a pattern inside it.
This is useful when you want to pass the full struct to a function but only care about one field for the match.
/// Represents a point in 2D space.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
/// Processes a point using @ binding.
fn process_point(p: Point) {
match p {
// Bind the whole point to `origin`.
// Check that x and y are both zero.
origin @ Point { x: 0, y: 0 } => println!("Origin: {:?}", origin),
// Bind the whole point to `p`.
// Check that x equals y.
p @ Point { x, y } if x == y => println!("Diagonal: {:?}", p),
// Fallback.
other => println!("Normal point: {:?}", other),
}
}
fn main() {
process_point(Point { x: 0, y: 0 });
process_point(Point { x: 5, y: 5 });
process_point(Point { x: 3, y: 7 });
}
Use @ when you need the whole value but only care about part of it for the match.
Pitfalls and compiler errors
Nested patterns are powerful, but they come with traps. The compiler catches most of them, but understanding the errors saves time.
Non-exhaustive patterns. If you forget a case, the compiler rejects the code with E0004 (non-exhaustive patterns). This happens even with nesting. If you match Ok(Some(user)) but forget Ok(None), the compiler points out the missing arm. It will not let you ship code that panics on a missing value.
Tuple field order. Tuples are ordered. Tuple(a, b) binds a to the first element and b to the second. If you swap them, the types might mismatch. The compiler throws E0308 (mismatched types) if the types differ. If the types are the same, you get a logic error. The compiler cannot catch swapped values of the same type.
Struct field renaming. You can rename bindings in struct patterns. Message::Move { x: new_x, y: new_y } binds x to new_x. This is useful when the field name conflicts with an existing variable. Forgetting to rename when needed causes shadowing warnings or confusion.
Guard evaluation. Guards run only if the pattern matches. If you put expensive logic in a guard, it still runs only for matching variants. However, guards can have side effects. Avoid side effects in guards. They make code harder to reason about.
Let the compiler enforce exhaustiveness. If it compiles, you handled every case.
Decision matrix
Use nested patterns when you need to inspect multiple layers of data in one go. Use nested patterns when you want to bind inner values directly without creating intermediate variables. Reach for if let when you only care about one specific nested case and want to ignore the rest with a simple fallback. Reach for sequential matches when the logic inside a branch is complex enough to warrant its own function. Use or patterns when multiple variants share identical logic and structure. Use guards when you need to check values in addition to structure. Use @ binding when you need the whole value but only care about part of it for the match.
Keep patterns flat. If you are nesting three levels deep, refactor the data, not the match.