Pattern Matching with Enums in Rust

Pattern matching with enums lets you destructure each variant and safely handle every possibility. Using the match expression, Rust forces you to deal with all cases, which helps prevent logic errors and unexpected behavior.

In Rust, pattern matching is how you interact with enums effectively. It gives you fine control over what each variant means and what to do with the data inside. If you are coming from JavaScript or TypeScript, think of it like a stricter version of a switch statement, one that makes sure you do not forget anything.

In this post, I will show you how to match on simple and complex enum variants, how to bind data inside them, and how to write cleaner matching logic with if let and matches!.

Matching Basic Enums

For simple enums without data:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

You can match like this:

let dir = Direction::Left;

match dir {
    Direction::Up => println!("Moving up"),
    Direction::Down => println!("Moving down"),
    Direction::Left => println!("Turning left"),
    Direction::Right => println!("Turning right"),
}

The compiler ensures you match all variants, which is a huge safety benefit.

Matching Enums with Data

If your enum carries data, match will extract the values for you:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Text(String),
    ChangeColor(u8, u8, u8),
}

let msg = Message::Move { x: 5, y: 10 };

match msg {
    Message::Quit => println!("Quit"),
    Message::Move { x, y } => println!("Move to {}, {}", x, y),
    Message::Text(text) => println!("Text: {}", text),
    Message::ChangeColor(r, g, b) => println!("Color: {}, {}, {}", r, g, b),
}

Rust automatically binds the fields so you can use them directly.

Ignoring Values

If you do not care about a value, use _:

match msg {
    Message::Move { x, .. } => println!("X: {}", x),
    _ => (),
}

You can also ignore entire variants:

match msg {
    Message::Text(_) => println!("Got some text"),
    _ => println!("Other message"),
}

Using if let

When you only care about one specific case, if let is shorter than match:

if let Message::Text(s) = msg {
    println!("Text: {}", s);
}

This is more concise but does not force you to handle all other cases. Use it when you do not need to.

Using matches! Macro

If you just want a boolean check:

if matches!(msg, Message::Quit) {
    println!("User quit the app");
}

This is useful in conditional logic or tests.

Enums with Option and Result

This is where pattern matching really shines. Take the Option type:

let maybe_name: Option<String> = Some(String::from("Alice"));

match maybe_name {
    Some(name) => println!("Name: {}", name),
    None => println!("No name provided"),
}

The Result type works similarly:

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Cannot divide by zero"))
    } else {
        Ok(a / b)
    }
}

match divide(10.0, 2.0) {
    Ok(result) => println!("Result: {}", result),
    Err(e) => println!("Error: {}", e),
}

These patterns help you avoid nulls and runtime crashes.

Summary

Pattern matching is the most powerful and safe way to handle enums in Rust. It forces you to cover all cases and gives you clear, concise access to the values inside. Combined with enums, pattern matching replaces many fragile logic patterns found in other languages.