match Expressions in Rust
match is one of Rust's most powerful features. It is a pattern-matching
expression that compares a value against a series of patterns and executes the
code associated with the first pattern that matches. Think of it as a supercharged
switch statement — but exhaustive, expression-based, and capable of
destructuring complex data structures in place.
Basic match Syntax
A match expression consists of the keyword match, the value being tested
(the scrutinee), and a list of arms in curly braces. Each arm has a pattern,
a fat arrow =>, and code to run.
fn main() {
let number = 3;
match number {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
4 => println!("four"),
5 => println!("five"),
_ => println!("something else"), // wildcard — catches everything else
}
// Prints: three
}match Must Be Exhaustive
Rust requires that a match covers every possible value of the scrutinee's
type. If you miss any case, the code will not compile. This prevents an entire
class of bugs where unhandled values cause unexpected behavior at runtime.
fn describe(n: u8) -> &'static str {
match n {
0 => "zero",
1..=9 => "single digit",
10..=99 => "double digit",
_ => "three digits or more", // handles 100..=255
}
// Without the _ arm, the compiler would say:
// error[E0004]: non-exhaustive patterns: u8 values 100..=255 not covered
}
fn main() {
println!("{}", describe(0)); // zero
println!("{}", describe(7)); // single digit
println!("{}", describe(42)); // double digit
println!("{}", describe(200)); // three digits or more
}Matching Literals
You can match on any literal value: integers, characters, booleans, and string slices.
fn main() {
// Integer literals
let code = 404u32;
let message = match code {
200 => "OK",
301 => "Moved Permanently",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Unknown",
};
println!("{}: {}", code, message); // 404: Not Found
// Character literals
let ch = 'A';
match ch {
'a'..='z' => println!("lowercase letter"),
'A'..='Z' => println!("uppercase letter"),
'0'..='9' => println!("digit"),
_ => println!("other character"),
}
// Boolean
let flag = true;
match flag {
true => println!("flag is set"),
false => println!("flag is clear"),
}
}Matching Ranges
Use the inclusive range pattern ..= inside match arms to match a span of values
at once. Exclusive .. ranges are not supported in patterns — only ..=.
fn grade(score: u32) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
0..=59 => "F",
_ => "Invalid score",
}
}
fn age_group(age: u32) -> &'static str {
match age {
0..=12 => "child",
13..=17 => "teenager",
18..=64 => "adult",
65..= => "senior", // open-ended: 65 and above
// Note: the compiler still requires exhaustiveness
}
}
fn main() {
println!("{}", grade(95)); // A
println!("{}", grade(73)); // C
println!("{}", grade(40)); // F
}Matching Multiple Patterns with |
The pipe | lets you combine multiple patterns into one arm. This is cleaner
than repeating the same code for several values.
fn main() {
let day = 6u8; // 1=Mon, 2=Tue, ..., 6=Sat, 7=Sun
let kind = match day {
1 | 2 | 3 | 4 | 5 => "weekday",
6 | 7 => "weekend",
_ => "invalid",
};
println!("{}", kind); // weekend
// Combine ranges and literals
let n = 5i32;
match n {
0 => println!("zero"),
1 | 2 | 3 => println!("small positive"),
4..=10 => println!("medium positive"),
_ => println!("large or negative"),
}
// medium positive
}Destructuring in match
One of Rust's most useful match features is the ability to destructure a value while matching it — pulling out its inner fields directly in the pattern.
// Destructuring a tuple
fn classify_point(point: (i32, i32)) -> &'static str {
match point {
(0, 0) => "origin",
(x, 0) => "on the x-axis",
(0, y) => "on the y-axis",
(x, y) => "somewhere else",
}
}
// Destructuring a struct
struct Color { r: u8, g: u8, b: u8 }
fn describe_color(c: Color) -> &'static str {
match c {
Color { r: 255, g: 0, b: 0 } => "pure red",
Color { r: 0, g: 255, b: 0 } => "pure green",
Color { r: 0, g: 0, b: 255 } => "pure blue",
Color { r, g, b } if r == g && g == b => "shade of grey",
_ => "mixed color",
}
}
fn main() {
println!("{}", classify_point((0, 0))); // origin
println!("{}", classify_point((5, 0))); // on the x-axis
println!("{}", classify_point((3, 7))); // somewhere else
println!("{}", describe_color(Color { r: 255, g: 0, b: 0 })); // pure red
println!("{}", describe_color(Color { r: 128, g: 128, b: 128 })); // shade of grey
}Destructuring Enums
Pattern matching shines brightest with enums. Each variant can carry different
data, and match lets you access that data in a single, readable expression.
#[derive(Debug)]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
fn name(&self) -> &'static str {
match self {
Shape::Circle { .. } => "circle",
Shape::Rectangle { .. } => "rectangle",
Shape::Triangle { .. } => "triangle",
}
}
}
fn main() {
let shapes = vec![
Shape::Circle { radius: 5.0 },
Shape::Rectangle { width: 4.0, height: 6.0 },
Shape::Triangle { base: 3.0, height: 8.0 },
];
for shape in &shapes {
println!("{}: area = {:.2}", shape.name(), shape.area());
}
// circle: area = 78.54
// rectangle: area = 24.00
// triangle: area = 12.00
}Binding with @ (The @ Operator)
The @ operator in a match arm lets you bind the matched value to a name while
simultaneously testing it against a pattern. Without @, you must choose between
testing a range and capturing the value — you cannot do both at once.
fn describe_number(n: i32) -> String {
match n {
// Bind to 'small' AND verify it is in 1..=5
small @ 1..=5 => format!("{} is a small number", small),
// Bind to 'mid' AND verify it is in 6..=10
mid @ 6..=10 => format!("{} is a medium number", mid),
other => format!("{} is something else", other),
}
}
fn main() {
println!("{}", describe_number(3)); // 3 is a small number
println!("{}", describe_number(8)); // 8 is a medium number
println!("{}", describe_number(42)); // 42 is something else
// @ inside a struct pattern
#[derive(Debug)]
struct Point { x: i32, y: i32 }
let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y: n @ 1..=10 } => println!("y is {} (in range)", n),
Point { x, y } => println!("x={}, y={}", x, y),
}
// y is 7 (in range)
}Match Guards
A match guard is an extra if condition after a pattern. The arm only fires
if the pattern matches AND the guard condition is true. Guards are useful when the
pattern alone is not specific enough.
fn main() {
let pair = (2, -2);
match pair {
(x, y) if x == y => println!("equal: {} == {}", x, y),
(x, y) if x + y == 0 => println!("opposites: {} and {}", x, y),
(x, _) if x % 2 == 0 => println!("x is even: {}", x),
(x, y) => println!("other: {}, {}", x, y),
}
// opposites: 2 and -2
// Guards with multiple patterns
let n = 4;
match n {
x if x < 0 => println!("{} is negative", x),
0 => println!("zero"),
x if x % 2 == 0 => println!("{} is a positive even number", x),
x => println!("{} is a positive odd number", x),
}
// 4 is a positive even number
}match as an Expression
Like if, match is an expression — it produces a value. Every arm must return
the same type. This lets you assign the result of a match directly to a variable
or return it from a function.
fn http_status(code: u32) -> &'static str {
match code {
200 => "OK",
301 => "Moved Permanently",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Unknown",
}
}
fn main() {
// Assign from match
let lang = "Rust";
let paradigm = match lang {
"Haskell" | "Erlang" => "functional",
"Java" | "C++" => "object-oriented",
"Rust" | "C" => "systems",
_ => "general purpose",
};
println!("{} is a {} language.", lang, paradigm);
// Rust is a systems language.
// Use match inline
let code = 404u32;
println!("Status {}: {}", code, http_status(code));
// Status 404: Not Found
}Matching Option
Option<T> is the Rust type for nullable values. It has two variants: Some(T)
holding a value, and None for absence. match is the idiomatic way to handle
both cases safely, with no risk of a null pointer exception.
fn find_user(id: u32) -> Option<String> {
match id {
1 => Some(String::from("Alice")),
2 => Some(String::from("Bob")),
_ => None,
}
}
fn greet_user(id: u32) {
match find_user(id) {
Some(name) => println!("Hello, {}!", name),
None => println!("User {} not found.", id),
}
}
fn main() {
greet_user(1); // Hello, Alice!
greet_user(2); // Hello, Bob!
greet_user(9); // User 9 not found.
// You can nest Some patterns to drill into optional values
let maybe_number: Option<Option<i32>> = Some(Some(42));
match maybe_number {
Some(Some(n)) => println!("Got {}", n), // Got 42
Some(None) => println!("Outer Some, inner None"),
None => println!("Nothing at all"),
}
}Matching Result
Result<T, E> is Rust's type for operations that can fail. It has variants
Ok(T) for success and Err(E) for failure. match on Result forces you
to handle both outcomes explicitly.
use std::num::ParseIntError;
fn parse_age(s: &str) -> Result<u32, ParseIntError> {
s.trim().parse::<u32>()
}
fn main() {
let inputs = vec!["25", " 30 ", "abc", "-5", "42"];
for input in inputs {
match parse_age(input) {
Ok(age) if age > 120 => println!("{:?} => implausibly old: {}", input, age),
Ok(age) => println!("{:?} => valid age: {}", input, age),
Err(e) => println!("{:?} => parse error: {}", input, e),
}
}
// "25" => valid age: 25
// " 30 " => valid age: 30
// "abc" => parse error: invalid digit found in string
// "-5" => parse error: invalid digit found in string
// "42" => valid age: 42
}Nested Patterns
Patterns can be nested arbitrarily deep. This lets you match on the structure of complex data in a single readable expression.
#[derive(Debug)]
enum Command {
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
Quit,
}
fn process(cmd: Command) {
match cmd {
Command::Quit =>
println!("Quitting"),
Command::Move { x, y } =>
println!("Moving to ({}, {})", x, y),
Command::Write(text) =>
println!("Writing: {}", text),
// Destructure the tuple inside the variant
Command::ChangeColor(r, g, b) =>
println!("Color: rgb({}, {}, {})", r, g, b),
}
}
fn main() {
process(Command::Move { x: 10, y: 20 });
process(Command::Write(String::from("hello")));
process(Command::ChangeColor(255, 128, 0));
process(Command::Quit);
}Why Exhaustive Matching Prevents Bugs
Consider what happens in other languages when you add a new enum variant: every switch/if-else that handles that enum silently ignores the new variant, which may cause undefined behavior or incorrect results.
In Rust, adding a new variant to an enum breaks every match on that enum with
a compile error, forcing you to handle the new case everywhere. This is called
exhaustiveness checking and it is one of Rust's most impactful correctness
guarantees.
// Suppose we add a new variant to this enum:
#[derive(Debug)]
enum Direction {
North,
South,
East,
West,
// New: NorthEast,
}
fn describe(d: &Direction) -> &'static str {
match d {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
// If NorthEast is added, the compiler immediately reports:
// error[E0004]: non-exhaustive patterns: Direction::NorthEast not covered
// This forces every match to be updated — no silent bug.
}
}
fn main() {
println!("{}", describe(&Direction::North));
}match vs if-else Chains
Aspect | match | if-else |
|---|---|---|
Exhaustiveness check | Yes — compiler enforces all cases | No — easy to forget a case |
Pattern destructuring | Yes — built in | No — must do separately |
Range matching | Yes — 1..=10 in patterns | Possible but verbose |
Multiple values per arm | Yes — with | | Possible but verbose |
Match guards | Yes — if condition after pattern | Equivalent to if-else |
Returns a value | Yes — expression | Yes — expression |
Works with enums | Ideal | Works but misses structure |
Best for | Single value, many cases, enums | Complex conditions, multiple variables |