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
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
}
-
match— the keyword -
<value>— the thing you're checking - each
pattern => code— one arm -
_— the catch-all (likedefaultin 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
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
💡
90..=100means "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!
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!
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
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
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
Since b is 0, the function returns Err(...), and our match catches it cleanly. No crashes, no panics!
💡 Option vs Result:
- Use
Optionwhen a value might not exist.- Use
Resultwhen 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 => codepair 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
- Write a function that returns
Result<i32, String>and handle it withmatch - Look into
if let— a shorthand for when you only care about one pattern - Explore
Optionmethods like.unwrap_or()and.map()— they usematchunder 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)