Skip to main content

Pattern Matching

Pattern matching is one of Rust's most powerful features. You got a taste of match in chapters 3 and 6 - now we go deep. By the end of this chapter, you will use pattern matching fluently to destructure data, handle variants, and write expressive conditional logic.

match is exhaustive​

The match expression must cover every possible value of the type being matched. The compiler enforces this:

enum TrafficLight {
Red,
Yellow,
Green,
}

fn action(light: &TrafficLight) -> &str {
match light {
TrafficLight::Red => "stop",
TrafficLight::Yellow => "caution",
TrafficLight::Green => "go",
}
}

fn main() {
let light = TrafficLight::Green;
println!("{}", action(&light));
}

If you remove one arm, the compiler rejects it:

error[E0004]: non-exhaustive patterns: `TrafficLight::Green` not covered

This is a safety feature - you can never forget to handle a case. If you add a new variant to an enum, the compiler tells you every match that needs updating.

The wildcard pattern _​

When you do not want to list every case, use _ as a catch-all:

fn describe_number(n: i32) -> &'static str {
match n {
0 => "zero",
1 => "one",
2 => "two",
_ => "many",
}
}

fn main() {
println!("{}", describe_number(0)); // zero
println!("{}", describe_number(42)); // many
}

_ matches anything and is used as the last arm. If you want to bind the value without using it, prefix with _:

fn main() {
let pair = (1, 2);

match pair {
(0, _) => println!("First is zero"),
(_, 0) => println!("Second is zero"),
_ => println!("Neither is zero"),
}
}

Destructuring​

Patterns can pull apart (destructure) structs, enums, tuples, and references.

Destructuring tuples​

fn main() {
let point = (3, -7);

match point {
(0, 0) => println!("Origin"),
(x, 0) => println!("On x-axis at {x}"),
(0, y) => println!("On y-axis at {y}"),
(x, y) => println!("Point at ({x}, {y})"),
}
}

Destructuring structs​

struct Point {
x: i32,
y: i32,
}

fn main() {
let point = Point { x: 5, y: -3 };

match point {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x, y: 0 } => println!("On x-axis at {x}"),
Point { x: 0, y } => println!("On y-axis at {y}"),
Point { x, y } => println!("({x}, {y})"),
}
}

You can also destructure with let:

struct Point {
x: i32,
y: i32,
}

fn main() {
let point = Point { x: 10, y: 20 };
let Point { x, y } = point;
println!("x = {x}, y = {y}");
}

Destructuring enums​

This is where pattern matching truly shines:

#[derive(Debug)]
enum Message {
Quit,
Echo(String),
Move { x: i32, y: i32 },
Color(u8, u8, u8),
}

fn process(msg: &Message) {
match msg {
Message::Quit => println!("Quitting"),
Message::Echo(text) => println!("Echo: {text}"),
Message::Move { x, y } => println!("Moving to ({x}, {y})"),
Message::Color(r, g, b) => println!("Color: rgb({r}, {g}, {b})"),
}
}

fn main() {
let messages = [
Message::Echo(String::from("hello")),
Message::Move { x: 10, y: 20 },
Message::Color(255, 128, 0),
Message::Quit,
];

for msg in &messages {
process(msg);
}
}

Nested destructuring​

Patterns can be nested arbitrarily deep:

struct Point {
x: i32,
y: i32,
}

enum Shape {
Circle { center: Point, radius: f64 },
Rect { top_left: Point, bottom_right: Point },
}

fn describe(shape: &Shape) {
match shape {
Shape::Circle {
center: Point { x, y },
radius,
} => println!("Circle at ({x}, {y}) with radius {radius}"),
Shape::Rect {
top_left: Point { x: x1, y: y1 },
bottom_right: Point { x: x2, y: y2 },
} => println!("Rectangle from ({x1}, {y1}) to ({x2}, {y2})"),
}
}

fn main() {
let shape = Shape::Circle {
center: Point { x: 0, y: 0 },
radius: 5.0,
};
describe(&shape);
}

Multiple patterns with |​

Use | (or) to match any of several patterns:

fn main() {
let number = 4;

match number {
1 | 2 => println!("one or two"),
3 | 4 => println!("three or four"),
_ => println!("something else"),
}
}

Match guards​

Add an if condition to a match arm for extra filtering:

fn main() {
let number = 4;

match number {
n if n < 0 => println!("{n} is negative"),
n if n == 0 => println!("zero"),
n if n % 2 == 0 => println!("{n} is positive and even"),
n => println!("{n} is positive and odd"),
}
}

Guards are checked after the pattern matches. They do not count as exhaustive coverage, so you usually still need a catch-all arm.

Binding with @​

The @ operator lets you bind a value to a name while also testing it against a pattern:

fn main() {
let age = 25;

match age {
n @ 0..=12 => println!("{n} - child"),
n @ 13..=17 => println!("{n} - teenager"),
n @ 18..=64 => println!("{n} - adult"),
n @ 65.. => println!("{n} - senior"),
_ => unreachable!(),
}
}

Without @, you would have to use a guard: 18..=64 if true => ... and then you would not have the value bound.

if let - matching a single pattern​

When you only care about one variant and want to ignore the rest, if let is more concise than match:

fn main() {
let maybe_number: Option<i32> = Some(42);

// With match
match maybe_number {
Some(n) => println!("Got {n}"),
None => {} // Do nothing
}

// Equivalent with if let
if let Some(n) = maybe_number {
println!("Got {n}");
}
}

if let is syntactic sugar for a match with one arm and a wildcard for everything else. You can add an else:

fn main() {
let config_value: Option<&str> = None;

if let Some(val) = config_value {
println!("Config: {val}");
} else {
println!("Using default config");
}
}

Use if let when:

  • You only care about one variant
  • A full match would have an empty _ => {} arm
  • The code is more readable without listing every case

Use match when:

  • You need to handle multiple variants
  • You want the compiler to enforce exhaustiveness

while let - looping on a pattern​

while let loops as long as a pattern matches:

fn main() {
let mut stack = vec![1, 2, 3, 4, 5];

while let Some(top) = stack.pop() {
println!("{top}");
}
// Prints: 5, 4, 3, 2, 1
}

Vec::pop() returns Option<T> - Some(value) when there are elements, None when empty. The while let keeps going until pop() returns None.

let else - early exit on pattern failure​

let else (stabilized in Rust 1.65) lets you bind a pattern or diverge:

fn process_name(name: Option<&str>) {
let Some(name) = name else {
println!("No name provided");
return;
};

println!("Processing: {name}");
}

fn main() {
process_name(Some("Alice"));
process_name(None);
}

The else branch must diverge - it must return, break, continue, or panic!. This is great for reducing nesting when you want to "unwrap or bail out early".

The matches! macro​

The matches! macro returns true if a value matches a pattern:

#[derive(Debug)]
enum Status {
Active,
Inactive,
Banned,
}

fn main() {
let status = Status::Active;

let is_active = matches!(status, Status::Active);
let is_restricted = matches!(status, Status::Inactive | Status::Banned);

println!("Active: {is_active}");
println!("Restricted: {is_restricted}");
}

This is a concise alternative to writing a match that returns true or false.

Matching on Option and Result​

Since Option and Result are enums, pattern matching works naturally:

Option patterns​

fn describe_option(opt: Option<i32>) {
match opt {
Some(0) => println!("Zero"),
Some(n) if n > 0 => println!("Positive: {n}"),
Some(n) => println!("Negative: {n}"),
None => println!("Nothing"),
}
}

fn main() {
describe_option(Some(42));
describe_option(Some(-3));
describe_option(Some(0));
describe_option(None);
}

Result patterns​

fn parse_and_double(input: &str) -> Result<i32, String> {
match input.parse::<i32>() {
Ok(n) => Ok(n * 2),
Err(e) => Err(format!("Failed to parse '{input}': {e}")),
}
}

fn main() {
match parse_and_double("21") {
Ok(n) => println!("Result: {n}"),
Err(e) => println!("Error: {e}"),
}

match parse_and_double("abc") {
Ok(n) => println!("Result: {n}"),
Err(e) => println!("Error: {e}"),
}
}

Patterns in function parameters​

Function parameters can be patterns too:

fn print_coordinates(&(x, y): &(i32, i32)) {
println!("({x}, {y})");
}

fn main() {
let point = (3, 5);
print_coordinates(&point);
}

A complete example​

Here is a small command processor that demonstrates many pattern matching techniques:

#[derive(Debug)]
enum Command {
Set { key: String, value: String },
Get(String),
Delete(String),
List,
Exit,
}

fn execute(cmd: &Command) {
match cmd {
Command::Set { key, value } => {
println!("SET {key} = {value}");
}
Command::Get(key) | Command::Delete(key) => {
let action = if matches!(cmd, Command::Get(_)) {
"GET"
} else {
"DELETE"
};
println!("{action} {key}");
}
Command::List => println!("LIST all keys"),
Command::Exit => println!("Goodbye!"),
}
}

fn main() {
let commands = [
Command::Set {
key: String::from("name"),
value: String::from("Alice"),
},
Command::Get(String::from("name")),
Command::List,
Command::Delete(String::from("name")),
Command::Exit,
];

for cmd in &commands {
execute(cmd);
}
}

Summary​

  • match is exhaustive - you must cover every possible value
  • _ is the wildcard that matches anything
  • Destructure tuples, structs, enums, and nested types in patterns
  • | matches multiple patterns; match guards add if conditions
  • @ binds a value while testing against a pattern
  • if let is concise for single-pattern matching
  • while let loops until a pattern no longer matches
  • let else binds a pattern or diverges (early exit)
  • matches! returns a boolean for quick pattern checks
  • Patterns work in match, if let, while let, let, function parameters, and for loops

Next up: Collections - Vec<T>, String, and HashMap<K, V> - the standard library's most-used data structures.