Skip to content

Intermediate · Structs, enums and pattern matching · topic 21 of 50 · 30 min

Pattern matching: match, if let and let else

Explanation

match compares a value against patterns and runs the arm that fits; it must cover every possible case, so you can never forget one. Patterns can bind values, use ranges, combine alternatives with | and add guards with if. For one interesting case, if let and let else are shorter, and while let loops while a pattern matches.

Why match?

Enums are only useful if you can act on each variant and get its data out. match does this. It is like a switch statement in other languages, but much stronger: it can look inside data, and it is exhaustive, which means the compiler checks that every possible case is handled.

rust
enum Payment {
    Upi(String),
    Card { last4: u16 },
    Cash,
}

fn describe(p: &Payment) -> String {
    match p {
        Payment::Upi(id) => format!("UPI from {id}"),
        Payment::Card { last4 } => format!("Card ending {last4}"),
        Payment::Cash => String::from("Cash"),
    }
}
Each arm is pattern => expression. id and last4 are bound to the data inside the variant.

A pattern describes the shape of a value. An arm is one pattern plus the code to run. The arms are tried from top to bottom, and the first matching arm runs. match is an expression, so all arms must produce the same type.

Exhaustiveness: the compiler remembers for you

output
match p {
    Payment::Upi(id) => println!("{id}"),
    Payment::Cash => println!("cash"),
}
// error[E0004]: non-exhaustive patterns: `&Payment::Card { .. }` not covered
If someone adds a new variant later, every match that forgot it stops compiling. This catches real bugs during changes.

More pattern tools

rust
fn ticket_price(age: u32) -> u32 {
    match age {
        0..=4 => 0,                 // a range
        5..=11 => 60,
        60 | 61 | 62 => 70,         // several values with |
        a if a >= 63 => 50,         // a guard: an extra condition
        _ => 100,                   // the catch-all
    }
}
_ matches anything and ignores it. Put it last; arms after it can never run.
rust
let point = (0, 7);
match point {
    (0, y) => println!("on the y axis at {y}"),
    (x, 0) => println!("on the x axis at {x}"),
    (x, y) => println!("at ({x}, {y})"),
}
Destructuring a tuple. Prints: on the y axis at 7.

if let: one interesting case

When you care about only one pattern, a full match with _ => () is long. if let is a shorter form. It runs the block only if the pattern matches. You can add else.

rust
let coupon: Option<u32> = Some(150);
if let Some(off) = coupon {
    println!("Rs {off} off");
} else {
    println!("No coupon");
}
Same as a match with Some(off) and None arms.

let else: get the value or leave

let else, stable since Rust 1.65, binds a value if the pattern matches; otherwise its else block runs, and that block must leave the current function or loop (return, break, continue or panic). It keeps the "happy path" un-indented.

rust
fn parse_qty(text: &str) -> u32 {
    let Ok(n) = text.trim().parse::<u32>() else {
        return 0;
    };
    n
}
If parsing fails, the function returns 0 right away. Otherwise n is available below.

while let and matches!

rust
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    print!("{top} ");        // 3 2 1
}
println!();

let status = 'S';
let is_vowel = matches!(status, 'a' | 'e' | 'i' | 'o' | 'u');
println!("{is_vowel}");      // false
while let loops until the pattern stops matching. matches! gives true or false for a pattern.

Binding modes in plain words

When you match on a reference, like &Payment, the bound variables are references too (id is a &String). This is why describe could take &Payment without moving anything out. If you match on an owned value, the data moves into the bound variables.

Common mistakes

  • Putting _ first: every later arm is unreachable, and the compiler warns. Put specific arms first.
  • Different types in different arms: match is an expression, so all arms must give the same type.
  • Matching an owned value and then using it again: the data was moved into the pattern. Match on a reference (&value) instead.
  • Using if let when two or more cases matter. Use a full match so the compiler can check them all.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Pattern matching: match, if let and let else · 1/7

match p {
    Payment::Upi(id) => ...,
    Payment::Cash => ...,
}

match compares a value against patterns and runs the first arm that fits.

Prefer a video by a teacher? Find free videos on “rust match pattern matching if let let else tutorial” on YouTube ↗

In real life: Order status messages

A food delivery app shows a different message for each order status, and some statuses carry data, like the rider's name or the reason for cancelling. A single match covers every status. When the product team adds a new status such as "Ready for pickup", every screen that forgot to handle it fails to compile, so no user ever sees a blank message.

rust
enum Status {
    Placed,
    Cooking { minutes_left: u32 },
    OutForDelivery(String),
    Delivered,
    Cancelled { reason: String },
}

fn message(s: &Status) -> String {
    match s {
        Status::Placed => "Order placed".to_string(),
        Status::Cooking { minutes_left } if *minutes_left <= 2 => "Almost ready".to_string(),
        Status::Cooking { minutes_left } => format!("Ready in {minutes_left} min"),
        Status::OutForDelivery(rider) => format!("{rider} is on the way"),
        Status::Delivered => "Enjoy your meal!".to_string(),
        Status::Cancelled { reason } => format!("Cancelled: {reason}"),
    }
}

fn main() {
    println!("{}", message(&Status::Cooking { minutes_left: 12 }));
    println!("{}", message(&Status::OutForDelivery(String::from("Imran"))));
}
Prints: Ready in 12 min, then Imran is on the way

Practice (0/3 done)

Task 1 · Question

What does this print? let n = 7; let kind = match n { 0 => "zero", 1..=5 => "small", x if x % 2 == 0 => "big even", _ => "big odd", }; println!("{kind}");

Task 2 · Question

Why does this not compile? enum Light { Red, Yellow, Green } fn go(l: Light) -> bool { match l { Light::Green => true, Light::Red => false, } }

Task 3 · Practice

Write enum Command { Add(u32), Remove(u32), Clear, Show } and fn run(cart: &mut Vec<u32>, cmd: Command) that uses match to change a cart of prices. Then write fn first_even(v: &[i32]) -> Option<i32>, and in main use let else to print "no even number" and return when it gives None.

Quick quiz

Question 1

1.What does "exhaustive" mean for match?

Question 2

2.What does the _ pattern do?

Question 3

3.When is if let better than match?

Question 4

4.In let Some(x) = opt else { ... };, what must the else block do?

Question 5

5.What does matches!(c, 'a'..='z') return?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does "exhaustive" mean for match?

Answer: Every possible value must be covered by some arm

The compiler checks that no case is forgotten.

What does the _ pattern do?

Answer: Matches anything and ignores it

_ is the catch-all pattern.

When is if let better than match?

Answer: When you care about only one pattern

if let is a short form for handling a single pattern.

In let Some(x) = opt else { ... };, what must the else block do?

Answer: Leave the current function or loop (return, break, continue or panic)

The else block must diverge, because x would otherwise have no value.

What does matches!(c, 'a'..='z') return?

Answer: true if c is a lowercase ASCII letter, else false

matches! turns a pattern test into a bool.

What does when do in a switch pattern?Related

Answer: Adds a guard condition to a case

The case matches only if the guard is true.

From Dart 3: enums, records, patterns and sealed classes

What does when do in a switch arm?Related

Answer: Adds an extra condition the arm must also satisfy

when is a case guard.

From Records and pattern matching

A switch case has code but no break. What happens?Related

Answer: Compile error: control cannot fall through

C# does not allow fall-through from a non-empty case (error CS0163).

From Decisions: if, else, switch and switch expressions

When does the else block of a for loop run?Related

Answer: Only when the loop finished without a break

Loop else means "no break happened". A break skips it.

From break, continue and loop else

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Structs, enums and pattern matching”. The module exam comes next.
  2. Module 5 exam: Structs, enums and pattern matching · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Error handling and collections. 50 topics left in the course before the final exam.

← Previous: Enums and Option