Now, let’s get into the weeds where things get interesting. You’ve seen how match arms can destructure a single enum, but what if your enum’s variants contain other enums? Or what if a simple pattern isn’t enough to express the precise condition you care about? This is where we graduate from simple pattern matching to the kind of expressive power that makes Rust feel like a superpower.

Matching Within Matching: The Nested Pattern

Imagine you’re modeling a complex system, like a graphic UI event. An event has a type (a mouse click, a key press), and that event itself has data. This is a classic case for nested enums.

enum MouseButton { Left, Right, Middle }
enum Key { Character(char), Enter, Escape }

enum UiEvent {
    MouseClick { x: i32, y: i32, button: MouseButton },
    KeyPress(Key),
    WindowClose,
}

Trying to handle this with a cascade of if let statements would be a messy, indented nightmare. Instead, you can match on the entire structure in one glorious, nested pattern.

fn handle_event(event: UiEvent) -> String {
    match event {
        // Nested pattern: We match on UiEvent::MouseClick, and within that,
        // we destructure its fields AND match on the inner MouseButton enum.
        UiEvent::MouseClick { x, y, button: MouseButton::Left } => {
            format!("Left click at ({}, {}). Primary action!", x, y)
        }
        UiEvent::MouseClick { x, y, button: MouseButton::Right } => {
            format!("Right click at ({}, {}). Context menu!", x, y)
        }
        // A catch-all for any other mouse button we haven't explicitly handled
        UiEvent::MouseClick { x, y, button } => {
            format!("Click with other button ({:?}) at ({}, {})", button, x, y)
        }
        // Another nested pattern: match on UiEvent::KeyPress and then on the inner Key
        UiEvent::KeyPress(Key::Character(' ')) => {
            "Spacebar pressed!".to_string()
        }
        UiEvent::KeyPress(Key::Enter) => {
            "Enter pressed!".to_string()
        }
        UiEvent::KeyPress(key) => {
            format!("Some other key pressed: {:?}", key)
        }
        UiEvent::WindowClose => {
            "Goodbye!".to_string()
        }
    }
}

fn main() {
    let event1 = UiEvent::MouseClick { x: 100, y: 200, button: MouseButton::Left };
    let event2 = UiEvent::KeyPress(Key::Character(' '));
    
    println!("{}", handle_event(event1)); // Left click at (100, 200). Primary action!
    println!("{}", handle_event(event2)); // Spacebar pressed!
}

See how we drilled down precisely to a left click or a spacebar press in a single, readable line? This composability is why Rust’s enums are so devastatingly effective. You’re not just checking types; you’re describing the exact shape of the data you want to handle.

When Patterns Aren’t Enough: Enter the Guard

But what if your condition is based on logic, not just structure? What if you care about the value of a bound variable? For example, “I want to handle a Key::Character, but only if it’s a numeric digit.” You could match on Key::Character(c) and then have a big if block inside the arm. It would work, but it’s clunky.

This is where the if guard comes in. It’s a conditional expression tacked onto the end of a pattern with an if keyword. The arm only matches if the pattern matches and the guard condition evaluates to true.

fn handle_numeric_input(event: UiEvent) -> Option<u32> {
    match event {
        // Pattern: It must be a KeyPress containing a Key::Character.
        // Guard: The character must be a numeric digit.
        UiEvent::KeyPress(Key::Character(c)) if c.is_ascii_digit() => {
            Some(c.to_digit(10).unwrap()) // This is safe because of the guard
        }
        // This arm handles all other KeyPress events (non-digit characters)
        UiEvent::KeyPress(_) => {
            println!("Not a digit!");
            None
        }
        // This arm handles everything else
        _ => None
    }
}

fn main() {
    let event1 = UiEvent::KeyPress(Key::Character('7'));
    let event2 = UiEvent::KeyPress(Key::Character('x'));
    
    println!("{:?}", handle_numeric_input(event1)); // Some(7)
    println!("{:?}", handle_numeric_input(event2)); // Not a digit! None
}

The guard is your best friend for adding that final, precise bit of logic to a pattern without breaking the elegant flow of your match expression. It keeps the conditional logic right where it belongs: in the pattern arm itself.

The Order of Operations Pitfall

Here’s a classic “gotcha” that has bitten every Rust developer at least once. Match arms are checked in order. This seems obvious until you combine it with guards and catch-all patterns.

Consider this buggy code:

fn check_value(x: Option<i32>) -> String {
    match x {
        Some(x) if x > 10 => "Big number".to_string(),
        Some(x) => format!("Small number: {}", x), // This arm matches ANY Some!
        None => "No number".to_string(),
    }
}

println!("{}", check_value(Some(20))); // "Big number" - correct.
println!("{}", check_value(Some(5)));  // "Small number: 5" - correct.
println!("{}", check_value(None));     // "No number" - correct.

Now watch what happens if we get clever and reorder the arms:

fn check_value_bad(x: Option<i32>) -> String {
    match x {
        Some(x) => format!("Small number: {}", x), // 💥 This matches FIRST, always.
        Some(x) if x > 10 => "Big number".to_string(), // This arm is now unreachable!
        None => "No number".to_string(),
    }
}
// The compiler will actually save you here with a warning: "unreachable pattern"

The compiler is brilliantly vigilant about this. It will tell you you’ve been silly. Always listen to it. The rule is simple: Place more specific arms (especially those with guards) before more general ones.

Best Practices: Clarity Over Cleverness

Nested patterns and guards are powerful tools, not toys. With great power comes great responsibility to not write inscrutable hieroglyphics.

  1. Don’t Go Too Deep: Matching on Outer::Variant(Inner::Variant(Contained::Variant(...))) is possible, but if you find yourself three levels deep, ask if the code is still clear. Sometimes, breaking it into multiple matches or using if let is kinder to the next person (who might be you in six months).

  2. Guards are for Logic, Patterns are for Shape: Use the pattern to ensure the data is shaped correctly (Some(tuple), MyEnum::Variant { field }). Use the guard for everything else (if field > 10, if user.is_authenticated()). This separation keeps things clean.

  3. The _ Pattern is Your Friend, But Not Your Crutch: A catch-all _ arm at the end of a complex match is often necessary and good. But if you’re using a guard on a _ pattern (_ if some_condition), tread carefully. It’s a code smell that you might be trying to force logic into a match that would be clearer as an if/else chain. The match shines when it’s exhaustive and explicit; using _ with a guard often undermines that.