Rust 24 🦀 Control Flow Matching with the match Expression
The match expression is Rust’s most powerful control flow construct. It is not a switch statement. It is an expression that takes a value, compares it against a series of patterns, and executes the code associated with the first pattern that matches. Every arm of a match must return the same type, and the match itself evaluates to a value. The compiler checks exhaustiveness: if a possible value is not covered by any pattern, the code does not compile. This combination—pattern matching, value production, and exhaustiveness checking—makes match the primary tool for working with enums, Option, Result, and any data that has a fixed set of shapes.
This chapter covers the match expression in full. You will learn the syntax, the wide range of patterns available, how to destructure enums and structs, how to use guards and binding, and how matching interacts with ownership and references. You will see how match replaces long if-else chains and switch statements, and how the exhaustiveness check turns refactoring into a compiler-guided process.
Key point: match is an expression, not a statement. It produces a value. Every arm must produce the same type. The patterns must be exhaustive—every possible value of the matched type must be handled by some arm. This exhaustiveness is checked at compile time and is the feature that makes match a refactoring tool.
Why match exists
The switch limitation. A switch statement in C, Java, or JavaScript compares a value against a series of constants and executes the matching branch. It cannot destructure complex data. It cannot bind variables from the matched value. It does not check exhaustiveness. Rust’s match does all three, which makes it suitable not just for integer dispatch but for working with the algebraic data types that Rust programs are built from.
The destructuring problem. When you have an enum like Option<T> or a custom enum with data-carrying variants, you need a way to extract the data from the variant you have. match patterns destructure the value directly: Some(x) binds the inner value to x; Message::Move { x, y } binds the named fields. No manual accessor methods, no casts, no type checks. The pattern does the extraction.
The exhaustiveness problem. In languages without exhaustiveness checking, adding a new case to an enum or a new error code is a manual refactoring task. You have to find every place that handles the value and add a new branch. With match, the compiler finds them for you. A non-exhaustive match is a compile error, so adding a variant to an enum breaks every match that does not handle it. The compiler points to each one.
The expression problem. Rust’s if and match are expressions, meaning they produce values. This lets you write let x = match value { ... }; and have each arm return a value that becomes x. It also lets you use match inside other expressions, like the body of a function, without an intermediate variable. This is a small syntactic difference from statement-based matching, but it shapes how Rust code reads.
The guard problem. Sometimes a pattern matches but the arm should only execute under an additional condition. Match guards (if condition after the pattern) provide this. They let you express “this variant, but only when the value is in this range” without nesting if statements inside the arm.
a. Basic match syntax and patterns
The syntax is match value { pattern => expression, ... }. Each arm has a pattern, a fat arrow, and an expression. Arms are separated by commas. The last arm does not need a trailing comma if it is a block.
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1 => "one",
2..=9 => "single digit",
_ => "large",
}
}
The patterns here are literals (0, 1), a range (2..=9), and a wildcard (_). The match evaluates n, finds the first pattern that matches, and returns the corresponding string. The wildcard _ catches everything else and is required here because i32 has values that the other patterns do not cover.
The wildcard is not always necessary. If the patterns cover every possible value of the type, no wildcard is needed. This is common with enums, where the variants are finite and known.
enum Direction {
North,
South,
East,
West,
}
fn turn_right(d: Direction) -> Direction {
match d {
Direction::North => Direction::East,
Direction::East => Direction::South,
Direction::South => Direction::West,
Direction::West => Direction::North,
}
}
Every variant is handled, so no wildcard is needed. The compiler verifies this. If a variant were missing, the code would not compile.
Patterns can be combined with |:
fn is_weekend(day: &str) -> bool {
match day {
"Saturday" | "Sunday" => true,
_ => false,
}
}
The | pattern matches if either alternative matches. This is useful for grouping cases that share the same behavior.
Patterns can bind values. A simple binding uses an identifier:
let x = 5;
match x {
n => println!("Got {}", n),
}
The pattern n matches anything and binds it to n. This is almost never useful alone because the wildcard _ does the same without binding, but it is the basis for more complex patterns.
b. Destructuring enums, structs, and tuples
The real power of match is destructuring. Patterns can extract data from enums, structs, tuples, and references.
Enum destructuring:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("Color ({}, {}, {})", r, g, b),
}
}
Each arm destructures the variant. Tuple variants bind by position. Struct variants bind by field name. Unit variants need no binding.
Struct destructuring:
struct Point {
x: i32,
y: i32,
}
fn describe(p: Point) {
match p {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x: 0, y } => println!("On y-axis at {}", y),
Point { x, y: 0 } => println!("On x-axis at {}", x),
Point { x, y } => println!("At ({}, {})", x, y),
}
}
The pattern Point { x: 0, y: 0 } matches only the origin. Point { x: 0, y } matches any point with x == 0 and binds y. The shorthand Point { x, y } binds both fields to variables named x and y.
Tuple destructuring:
fn describe_pair(pair: (i32, &str)) {
match pair {
(0, _) => println!("First is zero"),
(_, "hello") => println!("Second is hello"),
(n, s) => println!("({}, {})", n, s),
}
}
Tuple patterns match positionally. The wildcard _ ignores a position.
Nested destructuring:
enum Shape {
Circle { center: Point, radius: f64 },
Rectangle { top_left: Point, bottom_right: Point },
}
fn describe(shape: Shape) {
match shape {
Shape::Circle { center: Point { x: 0, y: 0 }, radius } => {
println!("Circle at origin with radius {}", radius);
}
Shape::Circle { center, radius } => {
println!("Circle at ({}, {}) radius {}", center.x, center.y, radius);
}
Shape::Rectangle { top_left, bottom_right } => {
println!("Rectangle from ({}, {}) to ({}, {})",
top_left.x, top_left.y, bottom_right.x, bottom_right.y);
}
}
}
Patterns nest arbitrarily deep. The first arm matches a circle whose center is the origin. The second matches any other circle. The third matches a rectangle.
Reference destructuring:
When matching on a reference, the patterns can match through the reference. Rust’s default binding modes make this ergonomic:
fn process(msg: &Message) {
match msg {
Message::Write(text) => println!("Write: {}", text),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
_ => {}
}
}
Matching on &Message with patterns that look like they match Message works because Rust automatically adjusts the bindings to references. text is &String and x, y are &i32. This is called match ergonomics.
c. Guards, bindings, and advanced patterns
Match guards add conditions to arms:
fn classify(n: i32) -> &'static str {
match n {
n if n < 0 => "negative",
0 => "zero",
n if n % 2 == 0 => "positive even",
_ => "positive odd",
}
}
The guard if n < 0 is checked after the pattern n matches. If the guard is false, the next arm is tried. Guards can reference the bound variables and external state.
The @ operator binds a value while also testing a pattern:
fn describe_age(age: u32) -> String {
match age {
n @ 0..=12 => format!("child ({})", n),
n @ 13..=19 => format!("teenager ({})", n),
n @ 20..=64 => format!("adult ({})", n),
n => format!("senior ({})", n),
}
}
The pattern n @ 0..=12 matches any value in the range and binds it to n. Without @, the range would match but the value would not be captured.
The .. pattern ignores remaining fields:
struct Config {
host: String,
port: u16,
debug: bool,
}
fn describe(config: Config) {
match config {
Config { debug: true, .. } => println!("Debug mode"),
Config { port: 8080, .. } => println!("Default port"),
Config { .. } => println!("Custom config"),
}
}
The .. fills in fields that are not explicitly mentioned. This is useful when a struct has many fields and you only care about a few.
The ref keyword borrows a value instead of moving it. In modern Rust, match ergonomics often makes ref unnecessary, but it is still available:
let value = Some(String::from("hello"));
match value {
Some(ref s) => println!("{}", s),
None => {}
}
// value is still valid here because it was borrowed
Without ref, the pattern Some(s) would move the String out of value, making value unusable afterward. With ref, it borrows.
The ref mut variant borrows mutably:
let mut value = Some(String::from("hello"));
match value {
Some(ref mut s) => s.push_str(" world"),
None => {}
}
Ranges in patterns use ..= for inclusive ranges and .. for exclusive ranges. Character ranges work too:
fn classify_char(c: char) -> &'static str {
match c {
'a'..='z' => "lowercase",
'A'..='Z' => "uppercase",
'0'..='9' => "digit",
_ => "other",
}
}
The match expression can return a value from each arm, and the arms must agree on the type. This is what makes match an expression rather than a statement.
let description = match value {
Some(n) if n > 0 => "positive",
Some(n) if n < 0 => "negative",
Some(_) => "zero",
None => "nothing",
};
The description variable receives the string from whichever arm matched. This eliminates the need for a mutable variable assigned in each branch, a common pattern in statement-based languages.
Complete Example Session
// ============================================
// PART 1: BASIC MATCH WITH LITERALS
// ============================================
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1 => "one",
2..=9 => "single digit",
_ => "large",
}
}
// ============================================
// PART 2: ENUM MATCHING
// ============================================
enum Direction { North, South, East, West }
fn turn_right(d: Direction) -> Direction {
match d {
Direction::North => Direction::East,
Direction::East => Direction::South,
Direction::South => Direction::West,
Direction::West => Direction::North,
}
}
// ============================================
// PART 3: DESTRUCTURING ENUM VARIANTS
// ============================================
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
}
}
// ============================================
// PART 4: STRUCT DESTRUCTURING
// ============================================
struct Point { x: i32, y: i32 }
fn describe(p: Point) {
match p {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x: 0, y } => println!("Y-axis at {}", y),
Point { x, y: 0 } => println!("X-axis at {}", x),
Point { x, y } => println!("({}, {})", x, y),
}
}
// ============================================
// PART 5: TUPLE DESTRUCTURING
// ============================================
fn describe_pair(pair: (i32, &str)) {
match pair {
(0, _) => println!("First is zero"),
(_, "hello") => println!("Second is hello"),
(n, s) => println!("({}, {})", n, s),
}
}
// ============================================
// PART 6: MATCH GUARDS
// ============================================
fn classify(n: i32) -> &'static str {
match n {
n if n < 0 => "negative",
0 => "zero",
n if n % 2 == 0 => "positive even",
_ => "positive odd",
}
}
// ============================================
// PART 7: @ BINDING
// ============================================
fn describe_age(age: u32) -> String {
match age {
n @ 0..=12 => format!("child ({})", n),
n @ 13..=19 => format!("teenager ({})", n),
n @ 20..=64 => format!("adult ({})", n),
n => format!("senior ({})", n),
}
}
// ============================================
// PART 8: IGNORING FIELDS WITH ..
// ============================================
struct Config { host: String, port: u16, debug: bool }
fn describe(config: Config) {
match config {
Config { debug: true, .. } => println!("Debug mode"),
Config { port: 8080, .. } => println!("Default port"),
Config { .. } => println!("Custom"),
}
}
// ============================================
// PART 9: MATCH RETURNING VALUE
// ============================================
fn sign(n: i32) -> i32 {
match n {
n if n > 0 => 1,
n if n < 0 => -1,
_ => 0,
}
}
// ============================================
// PART 10: NESTED PATTERNS
// ============================================
enum Shape {
Circle { center: Point, radius: f64 },
Rectangle { top_left: Point, bottom_right: Point },
}
fn describe_shape(shape: Shape) {
match shape {
Shape::Circle { center: Point { x: 0, y: 0 }, radius } => {
println!("Circle at origin radius {}", radius);
}
Shape::Circle { center, radius } => {
println!("Circle at ({}, {}) radius {}", center.x, center.y, radius);
}
Shape::Rectangle { top_left, bottom_right } => {
println!("Rectangle ({}, {})-({}, {})",
top_left.x, top_left.y, bottom_right.x, bottom_right.y);
}
}
}
The ten parts covered basic literals, enum matching, destructuring enum variants, struct destructuring, tuple destructuring, guards, @ binding, .. ignoring, match as an expression, and nested patterns.
Quick Reference
Pattern Types
| Pattern | Matches | Example |
|---|---|---|
| Literal | Exact value | 0, "hello", true |
| Range | Value in range | 1..=9, 'a'..='z' |
| Wildcard | Anything | _ |
| Binding | Anything, binds | n |
| Enum variant | Variant | Some(x), Message::Quit |
| Struct | Struct fields | Point { x, y } |
| Tuple | Tuple positions | (a, b, c) |
| Or | Either | 1 | 2 | 3 |
| Guard | Pattern + condition | n if n > 0 |
@ binding | Pattern + bind | n @ 1..=9 |
| Rest | Ignore remaining | Point { x, .. } |
| Reference | Borrow | Some(ref x) |
Match Rules
| Rule | Requirement |
|---|---|
| Exhaustive | All possible values covered |
| Same type | All arms return same type |
| First match wins | Arms evaluated top to bottom |
| Wildcard position | Usually last |
| Guards | Evaluated after pattern matches |
| Expression | Produces a value |
Destructuring Forms
| Type | Pattern |
|---|---|
| Tuple struct | Wrapper(x, y) |
| Struct | Point { x, y } |
| Enum unit | Color::Red |
| Enum tuple | Some(value) |
| Enum struct | Message::Move { x, y } |
| Nested | Some(Point { x, y }) |
| Reference | &value or ref value |
Best Practices
✅ Do This:
// Match on enums exhaustively
match direction {
Direction::North => { }
Direction::South => { }
Direction::East => { }
Direction::West => { }
} // ✅
// Use match as an expression
let result = match value {
Some(n) => n * 2,
None => 0,
}; // ✅
// Use @ to bind and test
n @ 1..=9 => println!("digit {}", n), // ✅
// Use guards for additional conditions
n if n > 0 => "positive", // ✅
// Use .. to ignore fields
Config { debug: true, .. } => { } // ✅
// Use if let for single-variant cases
if let Some(x) = value { } // ✅
❌ Don’t Do This:
// Don't use wildcard to silence exhaustiveness
match direction {
Direction::North => { }
_ => { } // hides future variants // ❌
}
// Don't put wildcard before specific arms
match n {
_ => "other",
0 => "zero", // unreachable // ❌
}
// Don't use match for boolean conditions
match x > 0 {
true => "positive",
false => "non-positive",
} // ⚠️ use if
// Don't ignore guards order
match n {
n if n > 0 => "positive",
n if n > 100 => "big", // unreachable for n > 0 // ❌
_ => "other",
}
// Don't move out of borrowed data
match &value {
Some(s) => { } // s is &String, fine
None => { }
} // ✅
// Don't nest deeply
match a {
Some(b) => match b {
Some(c) => match c { } // use nested patterns // ⚠️
None => { }
},
None => { }
}
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Non-exhaustive match | Missing variant | Add arm or use wildcard |
| Unreachable pattern | Earlier arm catches all | Reorder arms |
| Move error | Pattern moves out of borrowed data | Use ref or match on reference |
| Type mismatch | Arms return different types | Make all arms same type |
| Guard always false | Logic error | Check guard condition |
@ binding not used | Forgot to reference the binding | Use n in the arm |
| Nested match unreadable | Deeply nested patterns | Flatten with @ or nested patterns |
Real-World Examples
1. Option Handling
match value {
Some(n) => println!("Got {}", n),
None => println!("Nothing"),
}
2. Result Handling
match result {
Ok(data) => process(data),
Err(e) => eprintln!("Error: {}", e),
}
3. Command Dispatch
match command {
Command::Quit => break,
Command::Move { x, y } => move_to(x, y),
Command::Write(text) => write(text),
}
4. State Machine
match state {
State::Idle => State::Running,
State::Running => State::Done,
State::Done => State::Idle,
}
5. JSON Parsing
match json {
Json::Null => Value::Null,
Json::Bool(b) => Value::Bool(b),
Json::Number(n) => Value::Number(n),
Json::String(s) => Value::String(s),
Json::Array(a) => Value::Array(convert(a)),
Json::Object(o) => Value::Object(convert(o)),
}
6. HTTP Status
match status {
200 => "OK",
301 | 302 => "Redirect",
404 => "Not Found",
500..=599 => "Server Error",
_ => "Unknown",
}
7. Range Classification
match score {
n @ 90..=100 => "A",
n @ 80..=89 => "B",
n @ 70..=79 => "C",
_ => "F",
}
8. Nested Option
match outer {
Some(Some(inner)) => use_inner(inner),
Some(None) => println!("inner is none"),
None => println!("outer is none"),
}
9. Tuple Return
match pair {
(0, _) => "first zero",
(_, 0) => "second zero",
(a, b) => "both non-zero",
}
10. Reference Matching
match &value {
Some(ref s) if s.len() > 5 => println!("long"),
Some(_) => println!("short"),
None => println!("none"),
}
Visual
Match Evaluation Flow
┌─────────────────────────────────────────────────────────────┐
│ MATCH EVALUATION │
│ │
│ match value { │
│ Pattern1 => Result1, │
│ Pattern2 if guard => Result2, │
│ Pattern3 => Result3, │
│ _ => Result4, │
│ } │
│ │
│ 1. Evaluate `value` │
│ 2. Try Pattern1 │
│ ├── Match ──▶ execute Result1 │
│ └── No match ──▶ next │
│ 3. Try Pattern2 │
│ ├── Match pattern, check guard │
│ │ ├── Guard true ──▶ execute Result2 │
│ │ └── Guard false ──▶ next │
│ └── No match ──▶ next │
│ 4. Try Pattern3 │
│ 5. Try wildcard │
│ │
│ First matching arm wins. All arms same type. │
│ │
└─────────────────────────────────────────────────────────────┘
Pattern Types Visualized
┌─────────────────────────────────────────────────────────────┐
│ LITERAL PATTERN │
│ match n { 0 => "zero", 1 => "one", _ => "other" } │
│ ─────────── ───────────────────────────────── │
│ Compares for equality │
│ │
│ RANGE PATTERN │
│ match n { 1..=9 => "digit", _ => "other" } │
│ ──────────────── │
│ Checks membership in range │
│ │
│ ENUM PATTERN │
│ match opt { Some(x) => x, None => 0 } │
│ ─────────────── ────────────── │
│ Matches variant, binds data │
│ │
│ STRUCT PATTERN │
│ match p { Point { x: 0, y } => y, _ => 0 } │
│ ────────────────────────────── │
│ Matches field values, binds others │
│ │
│ GUARD │
│ match n { x if x > 0 => "pos", _ => "non-pos" } │
│ ──────────────────────── │
│ Pattern then condition │
│ │
│ @ BINDING │
│ match n { digit @ 0..=9 => digit, _ => 0 } │
│ ────────────────────────────── │
│ Test pattern and bind value │
│ │
└─────────────────────────────────────────────────────────────┘
Exhaustiveness Check
┌─────────────────────────────────────────────────────────────┐
│ EXHAUSTIVENESS │
│ │
│ enum Color { Red, Green, Blue } │
│ │
│ match c { │
│ Color::Red => { } │
│ Color::Green => { } │
│ } │
│ ✗ Error: Blue not covered │
│ │
│ match c { │
│ Color::Red => { } │
│ Color::Green => { } │
│ Color::Blue => { } │
│ } │
│ ✓ All variants handled │
│ │
│ Adding Color::Yellow later breaks all incomplete matches. │
│ The compiler points to each one. │
│ │
└─────────────────────────────────────────────────────────────┘
Match vs If-Else
┌─────────────────────────────────────────────────────────────┐
│ IF-ELSE CHAIN │
│ │
│ let result; │
│ if x == 0 { │
│ result = "zero"; │
│ } else if x == 1 { │
│ result = "one"; │
│ } else if x > 1 { │
│ result = "many"; │
│ } else { │
│ result = "negative"; │
│ } │
│ │
│ Statement-based, mutable binding, no exhaustiveness. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ MATCH EXPRESSION │
│ │
│ let result = match x { │
│ 0 => "zero", │
│ 1 => "one", │
│ n if n > 1 => "many", │
│ _ => "negative", │
│ }; │
│ │
│ Expression-based, immutable binding, exhaustiveness. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Syntax | match value { pattern => expr, ... } |
| Type | Expression, not statement |
| Exhaustiveness | Required by compiler |
| First match | First matching arm wins |
| Wildcard | _ matches anything |
| Literal pattern | 0, "hello" |
| Range pattern | 1..=9 |
| Enum pattern | Some(x), Message::Quit |
| Struct pattern | Point { x, y } |
| Tuple pattern | (a, b) |
| Or pattern | 1 | 2 | 3 |
| Guard | n if n > 0 |
| Binding | n @ 1..=9 |
| Ignoring | Point { x, .. } |
| Reference | ref x, &value |
Key takeaways:
matchis an expression. It produces a value, and every arm must produce the same type. This makes it usable wherever a value is expected: assignments, function returns, and nested expressions.- Patterns destructure data.
Some(x)binds the inner value.Point { x, y }binds named fields.(a, b)binds tuple positions. Patterns can nest arbitrarily deep. - Exhaustiveness is checked. The compiler verifies that every possible value is covered. A non-exhaustive match is a compile error. This turns adding a new enum variant into a compiler-guided refactoring task.
- The wildcard
_catches everything. It is useful when you genuinely do not care about remaining cases, but it defeats the exhaustiveness check. Use it deliberately, not as a default. - Guards add conditions.
n if n > 0matches the pattern and then checks the condition. If the guard is false, the next arm is tried. @binds and tests simultaneously.n @ 1..=9matches a value in the range and binds it ton. Without@, the range would match but the value would not be captured...ignores remaining fields. In struct patterns,Point { x, .. }matches onxand ignores the rest. This is useful when a struct has many fields and you only care about a few.- Match ergonomics simplify reference matching. When matching on a reference, patterns that look like they match the value work because Rust automatically adjusts bindings to references. This is why
match &value { Some(x) => ... }bindsxas a reference.
Remember: match is Rust’s central control flow construct for working with data that has structure. It combines pattern matching, destructuring, guards, and exhaustiveness checking into one expression. Where other languages use switch statements or chains of if-else, Rust uses match. The exhaustiveness check is not just a safety feature; it is a maintenance tool. When you add a new variant to an enum, the compiler finds every place that needs updating. Combined with Option, Result, and user-defined enums, match is the mechanism that makes Rust’s algebraic data types usable and safe.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!