DEV Community

Syeed Talha
Syeed Talha

Posted on

Understanding the match Keyword in Rust with Examples

If you've ever written a long if/else chain and thought "there must be a cleaner way"match is Rust's answer. It lets you check a value against a list of patterns and run different code for each one.

By the end of this post, you'll understand exactly how match works, why Rust forces you to use it a certain way, and how it handles real-world things like errors.


What is match?

match compares a value against a list of patterns. The first matching pattern wins, and the code next to it runs.

Here's the simplest example — matching a number:

let number = 2;

match number {
    1 => println!("One"),
    2 => println!("Two"),
    3 => println!("Three"),
    _ => println!("Something else"),
}
// Output: Two
Enter fullscreen mode Exit fullscreen mode

Each pattern => code pair is called an arm. Rust checks arms from top to bottom and stops at the first match.


The anatomy of a match expression

match  <value>  {
    pattern1 => code to run,
    pattern2 => code to run,
    _        => code to run,  // catch-all
}
Enter fullscreen mode Exit fullscreen mode
  • match — the keyword
  • <value> — the thing you're checking
  • each pattern => code — one arm
  • _ — the catch-all (like default in other languages)

The catch-all pattern: _

Rust requires you to cover every possible value. If you only match 1, 2, and 3 — what happens when the number is 99? You must either list every possibility or use _ as a catch-all.

⚠️ Compile error without it! If you forget the catch-all and don't cover every possible value, Rust will refuse to compile your code. This is a feature — it prevents bugs.

let day = 5;

let name = match day {
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    4 => "Thursday",
    5 => "Friday",
    6 => "Saturday",
    7 => "Sunday",
    _ => "Invalid day",  // covers anything outside 1–7
};

println!("Day {} is {}", day, name);
// Output: Day 5 is Friday
Enter fullscreen mode Exit fullscreen mode

Notice something cool here — match returned a value and we stored it in name. That's because match is an expression in Rust, not just a statement.


Matching ranges

Instead of listing every number, you can match a whole range using ..= (inclusive range):

let score = 72;

let grade = match score {
    90..=100 => "A",   // matches 90, 91, 92 ... 100
    80..=89  => "B",
    70..=79  => "C",
    60..=69  => "D",
    _        => "F",
};

println!("Your grade is: {}", grade);
// Output: Your grade is: C
Enter fullscreen mode Exit fullscreen mode

💡 90..=100 means "90 to 100, including both endpoints". The = in ..= makes it inclusive.


Matching enums — the most common use

This is where match truly shines. Rust enums can have different variants, and match is the natural way to handle each one.

enum Direction {
    North,
    South,
    East,
    West,
}

let heading = Direction::North;

let message = match heading {
    Direction::North => "Going up!",
    Direction::South => "Going down!",
    Direction::East  => "Going right!",
    Direction::West  => "Going left!",
    // No _ needed — all 4 variants are covered!
};

println!("{}", message);
// Output: Going up!
Enter fullscreen mode Exit fullscreen mode

When you cover every enum variant, no catch-all is needed. And if you add a new variant later (say, NorthEast), Rust will immediately tell you everywhere in your code that you forgot to handle it.


Destructuring — unpacking values from enums

Enum variants can hold data. With match, you can unpack that data right in the pattern. This is called destructuring.

enum Message {
    Hello(String),   // holds a String
    Number(i32),     // holds an i32
    Quit,            // holds nothing
}

let msg = Message::Hello(String::from("Rustacean"));

match msg {
    Message::Hello(name) => println!("Hello, {}!", name),
    Message::Number(n)   => println!("Got number: {}", n),
    Message::Quit        => println!("Quit!"),
}
// Output: Hello, Rustacean!
Enter fullscreen mode Exit fullscreen mode

Message::Hello(name) both matches the variant and pulls out the inner String into a variable called name. One line does two things!


Matching Option<T>

Option is one of Rust's most important types. It represents a value that might or might not exist — either Some(value) or None. You'll use match with it all the time.

fn find_first_even(numbers: Vec<i32>) -> Option<i32> {
    for n in numbers {
        if n % 2 == 0 {
            return Some(n);  // found one!
        }
    }
    None  // nothing found
}

let result = find_first_even(vec![1, 3, 4, 7]);

match result {
    Some(n) => println!("First even number: {}", n),
    None    => println!("No even numbers found."),
}
// Output: First even number: 4
Enter fullscreen mode Exit fullscreen mode

Matching Result<T, E> — handling errors

Result is how Rust handles errors. A function that can fail returns either Ok(value) for success or Err(reason) for failure. match is the standard way to handle both cases.

Example 1 — password check

fn check_password(input: &str) -> Result<&str, &str> {
    if input == "secret123" {
        Ok("Access granted")
    } else {
        Err("Wrong password")
    }
}

fn main() {
    match check_password("secret123") {
        Ok(msg)  => println!("{}", msg),
        Err(err) => println!("{}", err),
    }
}
// Output: Access granted
Enter fullscreen mode Exit fullscreen mode

The Result<&str, &str> return type means: "this function returns either a success &str or an error &str". The match handles both cases and unpacks the inner value.

Example 2 — safe division

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("Can't divide by zero".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    let result = divide(10, 0);

    match result {
        Ok(value) => println!("Result is {}", value),
        Err(err)  => println!("Error: {}", err),
    }
}
// Output: Error: Can't divide by zero
Enter fullscreen mode Exit fullscreen mode

Since b is 0, the function returns Err(...), and our match catches it cleanly. No crashes, no panics!

💡 Option vs Result:

  • Use Option when a value might not exist.
  • Use Result when an operation can fail and you want to explain why.

match vs if/else — when to use which

Situation Use Why
Checking many specific values match Cleaner and exhaustive
Handling Option or Result match Unpacks values naturally
Simple true/false condition if/else More readable for one condition
Comparing with >, < if/else match doesn't support these directly
Matching enum variants match Compiler verifies all variants covered

Quick summary

Here's everything match gives you:

  • Patterns & arms — each pattern => code pair is an arm; Rust tries them top to bottom
  • Exhaustive — every possible value must be covered, or your code won't compile
  • Returns a value — you can write let x = match ... to capture the result
  • Destructuring — unpack data from inside enum variants right in the pattern

What to try next

  1. Write a function that returns Result<i32, String> and handle it with match
  2. Look into if let — a shorthand for when you only care about one pattern
  3. Explore Option methods like .unwrap_or() and .map() — they use match under the hood

That's it! match might feel different at first if you're coming from other languages, but once it clicks, you'll wonder how you ever lived without it. 🦀


Found this helpful? Drop a ❤️ or share it with someone learning Rust!

Top comments (0)