| |

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

PatternMatchesExample
LiteralExact value0, "hello", true
RangeValue in range1..=9, 'a'..='z'
WildcardAnything_
BindingAnything, bindsn
Enum variantVariantSome(x), Message::Quit
StructStruct fieldsPoint { x, y }
TupleTuple positions(a, b, c)
OrEither1 | 2 | 3
GuardPattern + conditionn if n > 0
@ bindingPattern + bindn @ 1..=9
RestIgnore remainingPoint { x, .. }
ReferenceBorrowSome(ref x)

Match Rules

RuleRequirement
ExhaustiveAll possible values covered
Same typeAll arms return same type
First match winsArms evaluated top to bottom
Wildcard positionUsually last
GuardsEvaluated after pattern matches
ExpressionProduces a value

Destructuring Forms

TypePattern
Tuple structWrapper(x, y)
StructPoint { x, y }
Enum unitColor::Red
Enum tupleSome(value)
Enum structMessage::Move { x, y }
NestedSome(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

PitfallWhy It HappensFix
Non-exhaustive matchMissing variantAdd arm or use wildcard
Unreachable patternEarlier arm catches allReorder arms
Move errorPattern moves out of borrowed dataUse ref or match on reference
Type mismatchArms return different typesMake all arms same type
Guard always falseLogic errorCheck guard condition
@ binding not usedForgot to reference the bindingUse n in the arm
Nested match unreadableDeeply nested patternsFlatten 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

ItemValue
Syntaxmatch value { pattern => expr, ... }
TypeExpression, not statement
ExhaustivenessRequired by compiler
First matchFirst matching arm wins
Wildcard_ matches anything
Literal pattern0, "hello"
Range pattern1..=9
Enum patternSome(x), Message::Quit
Struct patternPoint { x, y }
Tuple pattern(a, b)
Or pattern1 | 2 | 3
Guardn if n > 0
Bindingn @ 1..=9
IgnoringPoint { x, .. }
Referenceref x, &value

Key takeaways:

  • match is 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 > 0 matches the pattern and then checks the condition. If the guard is false, the next arm is tried.
  • @ binds and tests simultaneously. n @ 1..=9 matches a value in the range and binds it to n. Without @, the range would match but the value would not be captured.
  • .. ignores remaining fields. In struct patterns, Point { x, .. } matches on x and 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) => ... } binds x as 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!