Skip to content

Rust Pattern Matching

On the Match page you learned the basics: arms, _, grouping values with |, and ranges. That is only the start. A pattern in Rust can also look inside a value, pull out the parts you need, and check extra conditions — all in one line.

Think of a pattern like a cookie cutter. You press it onto a value. If the shape fits, Rust hands you the pieces inside. If it doesn’t fit, Rust tries the next arm. Once this clicks, you will use patterns everywhere: in match, in let, in function parameters, and in if let.

Sometimes the shape is not enough and you need an extra check. A match guard is an if condition written after the pattern. The arm only runs if the pattern fits and the condition is true.

fn main() {
let temperatures = [-5, 0, 18, 35];
for t in temperatures {
let feeling = match t {
t if t < 0 => "freezing",
0 => "exactly zero",
t if t < 25 => "comfortable",
_ => "hot",
};
println!("{t}°C is {feeling}");
}
}

Output:

-5°C is freezing
0°C is exactly zero
18°C is comfortable
35°C is hot
▶ Try it Yourself

Rust checks the arms from top to bottom, so order matters. 18 fails the first guard, isn’t 0, and passes t < 25, so it is “comfortable”.

Destructuring means breaking a value into its parts. For a tuple, write the pattern in the same shape as the tuple. For a struct, name the fields you want.

struct Point {
x: i32,
y: i32,
}
fn main() {
let pair = (3, -7);
match pair {
(0, 0) => println!("the origin"),
(x, 0) => println!("on the x-axis at {x}"),
(0, y) => println!("on the y-axis at {y}"),
(x, y) => println!("somewhere else: ({x}, {y})"),
}
let p = Point { x: 0, y: 5 };
match p {
Point { x: 0, y } => println!("on the y-axis, y = {y}"),
Point { x, y: 0 } => println!("on the x-axis, x = {x}"),
Point { x, y } => println!("at ({x}, {y})"),
}
}

Output:

somewhere else: (3, -7)
on the y-axis, y = 5
▶ Try it Yourself

Notice the mix. 0 is a fixed value that must match exactly. x and y are new variables that catch whatever is there. Point { x, y } is short for Point { x: x, y: y }.

This is where patterns really shine. An enum variant can carry data, and a pattern can unpack that data while it checks which variant you have.

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Square(f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Square(side) => side * side,
}
}
fn main() {
let shapes = [
Shape::Circle { radius: 2.0 },
Shape::Rectangle { width: 3.0, height: 4.5 },
Shape::Square(1.5),
];
for shape in &shapes {
println!("area = {:.2}", area(shape));
}
}

Output:

area = 12.57
area = 13.50
area = 2.25
▶ Try it Yourself

You can’t reach radius without first proving the value is a Circle. That is the safety win: the compiler won’t let you read data from the wrong variant.

Patterns can go inside other patterns, as deep as you like. Here a Command enum holds a Direction enum, and one arm matches both levels at once.

An @ binding lets you test a value against a pattern and keep it in a variable. n @ 1..=9 means “check that it is between 1 and 9, and call it n”.

enum Direction {
Up,
Down,
}
enum Command {
Move(Direction, u32),
Say(String),
Quit,
}
fn run(cmd: Command) {
match cmd {
Command::Move(Direction::Up, steps @ 1..=9) => println!("up {steps} (small step)"),
Command::Move(Direction::Up, steps) => println!("up {steps} (big jump!)"),
Command::Move(Direction::Down, steps) => println!("down {steps}"),
Command::Say(text) => println!("saying: {text}"),
Command::Quit => println!("bye"),
}
}
fn main() {
run(Command::Move(Direction::Up, 3));
run(Command::Move(Direction::Up, 50));
run(Command::Move(Direction::Down, 2));
run(Command::Say(String::from("hello")));
run(Command::Quit);
}

Output:

up 3 (small step)
up 50 (big jump!)
down 2
saying: hello
bye
▶ Try it Yourself

Without @, a range like 1..=9 checks the value but throws it away. With @, you get to keep it.

Often you only care about some of the pieces. Use _ to skip one value. Use .. to skip “everything else”.

struct Player {
name: String,
level: u32,
gold: u32,
}
fn main() {
let rgb = (255, 128, 0);
let (red, _, _) = rgb;
println!("red = {red}");
let numbers = (1, 2, 3, 4, 5);
let (first, .., last) = numbers;
println!("first = {first}, last = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 };
let Player { name, level, .. } = hero;
println!("{name} is level {level}");
}

Output:

red = 255
first = 1, last = 5
Mira is level 7
▶ Try it Yourself

.. can only appear once in a pattern. (first, .., last) is fine, but (.., middle, ..) is not — Rust couldn’t tell which value you meant.

Every let is secretly a pattern. So is every function parameter. That means you can destructure right where a value arrives.

fn print_coords(&(x, y): &(i32, i32)) {
println!("x: {x}, y: {y}");
}
fn main() {
let (width, height) = (1920, 1080);
println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)];
let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect();
println!("{:?}", sums);
}

Output:

1920x1080
x: 4, y: -2
[3, 7, 11]
▶ Try it Yourself

The closure |&(a, b)| a + b uses the same idea. Each item is a &(i32, i32), and the pattern unpacks it straight into a and b. You will see this a lot with iterators.

Two handy tools save you from writing a full match.

matches!(value, pattern) returns true or false. It is perfect inside if or .filter() when you only need a yes/no answer. Guards work here too.

let-else handles “unpack this, or leave now”. Write let PATTERN = value else { ... };. If the pattern fits, the variables are available for the rest of the function. If not, the else block runs, and it must leave — with return, break, continue, or a panic.

fn parse_age(input: &str) -> Option<u32> {
let Ok(age) = input.parse::<u32>() else {
println!("'{input}' is not a number");
return None;
};
Some(age)
}
fn main() {
let grade = 'B';
println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66];
let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count();
println!("{passed} passed");
println!("{:?}", parse_age("31"));
println!("{:?}", parse_age("thirty"));
}

Output:

true
3 passed
Some(31)
'thirty' is not a number
None
▶ Try it Yourself

let-else keeps the “happy path” flat. Instead of nesting your code inside if let ... { }, you deal with the bad case first and move on. Pair it with Option and Result values.

Guards don’t count for “all cases covered”. The compiler can’t reason about your if conditions, so this fails even though the two guards cover every number:

fn main() {
let n: i32 = 7;
match n {
x if x < 0 => println!("negative"),
x if x >= 0 => println!("zero or positive"),
}
}
error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered
--> src/main.rs:3:11
|
3 | match n {
| ^ pattern `i32::MIN..=i32::MAX` not covered
|
= note: the matched value is of type `i32`
= note: match arms with guards don't count towards exhaustivity

The fix: make the last arm a plain catch-all, such as _ => println!("zero or positive").

A variable name in a pattern is a new variable, not a comparison. This looks like it checks guess == expected, but it doesn’t:

fn main() {
let expected = 3;
let guess = 5;
match guess {
expected => println!("Correct!"),
_ => println!("Wrong"),
}
}

It compiles and prints Correct!, because expected in the arm just creates a new variable that catches any value. Rust warns you:

warning: unreachable pattern
--> src/main.rs:6:9
|
5 | expected => println!("Correct!"),
| -------- matches any value
6 | _ => println!("Wrong"),
| ^ no value can reach this

The fix is a guard: g if g == expected => println!("Correct!").

Print the numbers from 9 to 15, but print Fizz for multiples of 3, Buzz for multiples of 5, and FizzBuzz for multiples of both. Do it with one match on a tuple instead of an if chain. Hint: match on (n % 3, n % 5). A remainder of 0 means “divides evenly”, and _ means “don’t care”.

Show solution
fn main() {
for n in 9..=15 {
match (n % 3, n % 5) {
(0, 0) => println!("FizzBuzz"),
(0, _) => println!("Fizz"),
(_, 0) => println!("Buzz"),
_ => println!("{n}"),
}
}
}

Output:

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Try it Yourself

(0, 0) has to come first. If (0, _) were first, 15 would print Fizz.

Define enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Start a balance at 100, then apply this list in order: deposit 50, withdraw 30, withdraw 500, fee. A fee costs 5. A withdrawal bigger than the balance must be declined with a message instead. Print the final balance. Hint: use a match guard, Transaction::Withdraw(amount) if amount <= balance, followed by a second Withdraw arm for the declined case.

Show solution
enum Transaction {
Deposit(u32),
Withdraw(u32),
Fee,
}
fn main() {
let mut balance: u32 = 100;
let history = [
Transaction::Deposit(50),
Transaction::Withdraw(30),
Transaction::Withdraw(500),
Transaction::Fee,
];
for t in history {
match t {
Transaction::Deposit(amount) => balance += amount,
Transaction::Withdraw(amount) if amount <= balance => balance -= amount,
Transaction::Withdraw(amount) => println!("declined: can't withdraw {amount}"),
Transaction::Fee => balance -= 5,
}
}
println!("balance = {balance}");
}

Output:

declined: can't withdraw 500
balance = 115
▶ Try it Yourself

The second Withdraw arm has no guard, so it catches every withdrawal the first arm turned down. Without it, the match wouldn’t compile: guards don’t count when Rust checks that every case is covered.

  • A match guard (pattern if condition) adds an extra check to an arm.
  • Patterns can destructure tuples, structs, and enum variants, and they can be nested.
  • name @ pattern tests a value and keeps it in a variable at the same time.
  • _ skips one value; .. skips the rest. Patterns also work in let, function parameters, and closures.
  • matches! gives a quick true/false; let-else unpacks a value or leaves early.

Quick check

1. What does n @ 1..=9 do in a match arm?

2. In let-else, what must the else block do?

3. What does let (first, .., last) = (1, 2, 3, 4, 5); give you?

4. Why does a match with only guarded arms often fail to compile?

Score: 0 / 4