Skip to editor content
learningrust.orglesson 7 of 26

Pattern Matching in Rust

Pattern matching is one of Rust's most powerful features. It lets you compare a value against a series of patterns and execute code based on which pattern matches. Unlike simple switch statements in other languages, Rust's pattern matching is exhaustive, meaning the compiler ensures you handle every possible case. Combined with destructuring, it becomes an expressive tool for writing concise and safe code.

The match Expression

The match expression compares a value against a series of patterns. Each pattern is called an arm, and Rust guarantees that all possible values are covered:

fn main() {
    let number = 7;

    match number {
        1 => println!("One"),
        2 => println!("Two"),
        3 => println!("Three"),
        4..=6 => println!("Between four and six"),
        7 | 8 => println!("Seven or eight"),
        9..=20 => println!("Between nine and twenty"),
        _ => println!("Something else"),
    }
}

The _ pattern is a wildcard that matches anything. It is commonly used as the last arm to handle all remaining cases. The | operator lets you combine multiple patterns in a single arm, and ..= creates inclusive ranges.

match Returns a Value

Since match is an expression in Rust, it produces a value. This means you can assign the result of a match to a variable:

fn main() {
    let coin = "quarter";

    let value_in_cents = match coin {
        "penny" => 1,
        "nickel" => 5,
        "dime" => 10,
        "quarter" => 25,
        _ => {
            println!("Unknown coin: {}", coin);
            0
        }
    };

    println!("A {} is worth {} cents", coin, value_in_cents);
}

Notice that each arm must return the same type. If an arm has multiple statements, wrap them in braces and ensure the last expression is the return value.

Matching Enums

Pattern matching truly shines when working with enums. Because match is exhaustive, the compiler forces you to handle every variant, which prevents bugs caused by forgotten cases:

enum HttpStatus {
    Ok,
    NotFound,
    InternalError,
    Redirect(String),
    Custom(u16, String),
}

fn describe_status(status: HttpStatus) -> String {
    match status {
        HttpStatus::Ok => String::from("200 - Everything is fine"),
        HttpStatus::NotFound => String::from("404 - Resource not found"),
        HttpStatus::InternalError => String::from("500 - Server error"),
        HttpStatus::Redirect(url) => format!("301 - Redirecting to {}", url),
        HttpStatus::Custom(code, msg) => format!("{} - {}", code, msg),
    }
}

fn main() {
    let statuses = vec![
        HttpStatus::Ok,
        HttpStatus::NotFound,
        HttpStatus::Redirect(String::from("https://example.com")),
        HttpStatus::Custom(418, String::from("I'm a teapot")),
    ];

    for status in statuses {
        println!("{}", describe_status(status));
    }
}

Destructuring in Match Arms

You can destructure structs, tuples, and nested types directly inside match arms to extract the data you need:

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

fn classify_point(point: &Point) -> &str {
    match (point.x, point.y) {
        (0, 0) => "at the origin",
        (x, 0) if x > 0 => "on the positive x-axis",
        (0, y) if y > 0 => "on the positive y-axis",
        (x, y) if x > 0 && y > 0 => "in the first quadrant",
        (x, y) if x < 0 && y > 0 => "in the second quadrant",
        _ => "somewhere else",
    }
}

fn main() {
    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 5, y: 0 },
        Point { x: 3, y: 7 },
        Point { x: -2, y: 4 },
    ];

    for p in &points {
        println!("({}, {}) is {}", p.x, p.y, classify_point(p));
    }
}

Match Guards

A match guard is an additional if condition on a match arm. The arm only matches if both the pattern and the guard are satisfied:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    for &num in &numbers {
        let description = match num {
            n if n % 2 == 0 && n > 5 => "large even",
            n if n % 2 == 0 => "small even",
            n if n > 5 => "large odd",
            _ => "small odd",
        };
        println!("{} is {}", num, description);
    }
}

Guards are useful when you need to test conditions that cannot be expressed purely through patterns, like arithmetic comparisons or calling functions.

if let for Simple Patterns

When you only care about one pattern and want to ignore the rest, if let provides a cleaner syntax than a full match:

fn main() {
    let config_value: Option<u32> = Some(3);

    // With match - verbose for a single case
    match config_value {
        Some(val) => println!("Config value: {}", val),
        None => {}
    }

    // With if let - much cleaner
    if let Some(val) = config_value {
        println!("Config value (if let): {}", val);
    }

    // if let with else
    let missing: Option<u32> = None;
    if let Some(val) = missing {
        println!("Found: {}", val);
    } else {
        println!("No value configured, using default");
    }
}

while let for Iterative Matching

The while let construct repeatedly matches a pattern in a loop. It keeps running as long as the pattern continues to match, which is especially useful for consuming iterators or popping from collections:

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

    // Pop elements until the stack is empty
    while let Some(top) = stack.pop() {
        println!("Popped: {}", top);
    }

    println!("Stack is now empty: {:?}", stack);

    // while let with an iterator
    let names = vec!["Alice", "Bob", "Charlie"];
    let mut iter = names.iter();

    while let Some(name) = iter.next() {
        println!("Hello, {}!", name);
    }
}

Nested Pattern Matching

Patterns can be nested to match complex data structures. This is particularly powerful when working with enums that contain other enums or structs:

enum Command {
    Move { x: i32, y: i32 },
    Write(String),
    Quit,
}

enum AppEvent {
    UserCommand(Command),
    SystemAlert(String),
    Tick(u64),
}

fn handle_event(event: AppEvent) {
    match event {
        AppEvent::UserCommand(Command::Move { x, y }) => {
            println!("User moved to ({}, {})", x, y);
        }
        AppEvent::UserCommand(Command::Write(text)) => {
            println!("User wrote: {}", text);
        }
        AppEvent::UserCommand(Command::Quit) => {
            println!("User quit the application");
        }
        AppEvent::SystemAlert(msg) => {
            println!("ALERT: {}", msg);
        }
        AppEvent::Tick(ms) => {
            println!("Tick at {} ms", ms);
        }
    }
}

fn main() {
    let events = vec![
        AppEvent::UserCommand(Command::Move { x: 10, y: 20 }),
        AppEvent::SystemAlert(String::from("Low memory")),
        AppEvent::UserCommand(Command::Write(String::from("Hello!"))),
        AppEvent::Tick(1500),
        AppEvent::UserCommand(Command::Quit),
    ];

    for event in events {
        handle_event(event);
    }
}

Try It Yourself

Combine multiple pattern matching techniques in a single program. Try modifying the temperature thresholds or adding new weather conditions:

enum Temperature {
    Celsius(f64),
    Fahrenheit(f64),
}

fn to_celsius(temp: &Temperature) -> f64 {
    match temp {
        Temperature::Celsius(c) => *c,
        Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
    }
}

fn weather_advisory(temp: &Temperature) -> &str {
    let celsius = to_celsius(temp);
    match celsius as i32 {
        c if c < -10 => "Extreme cold warning! Stay indoors.",
        -10..=0 => "Freezing conditions. Bundle up!",
        1..=15 => "Cool weather. Wear a jacket.",
        16..=25 => "Pleasant weather. Enjoy your day!",
        26..=35 => "Hot weather. Stay hydrated.",
        _ => "Extreme heat warning! Seek shade.",
    }
}

fn main() {
    let readings = vec![
        Temperature::Celsius(22.0),
        Temperature::Fahrenheit(14.0),
        Temperature::Celsius(-15.0),
        Temperature::Fahrenheit(98.6),
    ];

    for temp in &readings {
        let celsius = to_celsius(temp);
        let original = match temp {
            Temperature::Celsius(c) => format!("{:.1} C", c),
            Temperature::Fahrenheit(f) => format!("{:.1} F", f),
        };
        println!("{} ({:.1} C): {}", original, celsius, weather_advisory(temp));
    }
}

Key Takeaways

  • match is exhaustive -- the compiler ensures you handle all possible cases
  • Patterns can include literals, ranges (..=), wildcards (_), and alternatives (|)
  • Match guards (if conditions) add extra flexibility to pattern arms
  • Destructuring extracts values from enums, structs, and tuples inside patterns
  • if let simplifies code when you only care about one pattern
  • while let repeats as long as a pattern matches, great for consuming iterators and stacks
  • Patterns can be nested to match deeply structured data

Pro Tip: If you find yourself writing a match with many arms where most do the same thing, consider using if let for the one case you care about, or restructure your code with methods on the enum to encapsulate the matching logic.

Next Steps

Now that you can destructure data with patterns, we'll go deeper into borrowing — the rules that govern how references let different parts of your program share data safely without copying it.

Next lesson

Borrowing in Depth

Deep dive into Rust borrowing and references — learn shared and mutable references, the borrow checker, and safe data access

25 min