7.3Introduction to match

Last updated June 12, 2026

Here's a function that names a digit, written with the tools you have so far:

fn print_digit_name(n: u32) {
    if n == 1 {
        println!("one");
    } else if n == 2 {
        println!("two");
    } else if n == 3 {
        println!("three");
    } else {
        println!("something else");
    }
}

fn main() {
    print_digit_name(2);
}
two

It works, but look at it: the same n == ritual on every line, with the interesting parts (which value, which response) buried in boilerplate. Testing one value against a series of possibilities is so common that Rust has a purpose-built expression for it.

match

Here's the same function, rewritten:

fn print_digit_name(n: u32) {
    match n {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("something else"),
    }
}

fn main() {
    print_digit_name(2);
}
two

A match takes a value to examine, called the scrutinee (n here, the thing being scrutinized), and a list of arms between the braces. Each arm has the form pattern => expression. The scrutinee is evaluated once, compared against each arm's pattern from top to bottom, and the first pattern that fits wins: its expression runs, and the match is over. No other arm is checked, and there's no way to "fall into" the next arm.

The patterns above are the simplest kind, literal values. The last arm's _ is the wildcard pattern: it matches anything, which makes it the "everything else" arm. Since arms are tried in order, the wildcard goes last; anything after it could never win.

If you're coming from a C-family language, this is the slot switch occupies, and the differences are all in match's favor. A switch only works on integer-like types; match works on almost anything, including strings and the structured types of chapters 10 and 11. A switch falls through from one case to the next unless you remember a break on every case, a default so treacherous that compilers warn about it; match arms are sealed rooms, no break required. And switch will happily let you forget a case. Which brings us to match's best feature.

Forgetting a case is a compile error

Delete the wildcard arm and try to compile:

fn print_digit_name(n: u32) {
    match n {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
    }
}

fn main() {
    print_digit_name(2);
}
error[E0004]: non-exhaustive patterns: `0_u32` and `4_u32..=u32::MAX` not covered
 --> src/main.rs:2:11
  |
2 |     match n {
  |           ^ patterns `0_u32` and `4_u32..=u32::MAX` not covered
  |
  = note: the matched value is of type `u32`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms
  |
5 ~         3 => println!("three"),
6 ~         0_u32 | 4_u32..=u32::MAX => todo!(),
  |

Read it with your practiced eye. A match must be exhaustive: its arms must cover every value the scrutinee's type can hold. Ours covers 1, 2, and 3, and the compiler computed precisely what's missing: zero, and everything from 4 up. It even drafted the fix.

New programmers sometimes read this as the compiler being fussy. It's the opposite: this is the compiler agreeing to remember your edge cases so you don't have to. Every if/else if chain silently hopes you thought of everything; a match proves you did, and re-proves it every compile, forever. When chapter 11 adds types with a fixed set of named cases, exhaustiveness becomes the feature you'd defend with a pitchfork: add a new case to the type, and the compiler lists every match in the program that needs updating.

Key insight

An if chain checks the cases you thought of. A match also checks the completeness of your thinking.

One arm, several patterns

Patterns can be combined with |, read "or":

fn is_vowel(c: char) -> bool {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' => true,
        _ => false,
    }
}

fn main() {
    println!("{}", is_vowel('e'));
    println!("{}", is_vowel('k'));
}
true
false

(C's switch fakes this by stacking case labels and exploiting fallthrough, the same mechanism that causes its bugs. Here it's just syntax.)

match is an expression

You saw this coming. Like if, like blocks, like everything in this language, a match produces a value: the value of whichever arm ran. is_vowel above already leans on this (the match is the function's tail expression). It works in let position too:

fn main() {
    let lane = 3;
    let speed_limit = match lane {
        1 => 60,
        2 => 80,
        _ => 100,
    };
    println!("lane {lane}: limit {speed_limit}");
}
lane 3: limit 100

And the same rule from lesson 4.7 follows: in value position, every arm must produce the same type. Make one arm a &str and another an integer, and you'll get the familiar E0308, pointing at the odd arm out.

What we're saving for later

Patterns are a much bigger country than literals and _. They can capture parts of a value into variables, take apart tuples and structs, and carry extra if conditions. All of that arrives in chapter 11, where match meets the types it was born for (and where Ordering, a type you'll use in this chapter's project on faith, finally makes full sense).

Key insight

For now, the working rule: reach for match when you're asking "which of these specific values is it?", and for if when you're asking about ranges and arbitrary conditions. Lesson 7.10 uses both, each where it's strongest.

Quiz time

Question #1

What does this print?

fn main() {
    let n = 7;
    let label = match n % 3 {
        0 => "fizz",
        1 => "one over",
        _ => "two over",
    };
    println!("{label}");
}
Show solution
one over

7 % 3 is 1, which matches the literal pattern 1. The match is an expression, so label receives "one over". Note the arms agree on type (all &str), as they must.

Question #2

Predict the compiler's reaction, precisely:

fn main() {
    let coin = 'q';
    match coin {
        'h' => println!("heads"),
        't' => println!("tails"),
    }
}
Show solution

E0004: non-exhaustive patterns. A char can be far more than 'h' and 't', and a match must cover every possibility. The fix is a final arm, for example _ => println!("not a coin"). (Whether 'q' would have matched at runtime is irrelevant; exhaustiveness is checked at compile time, against the type.)

Question #3

Write a function calculate(x: i32, y: i32, op: char) that prints the result of applying op to x and y, supporting +, -, *, /, and % (the remainder operator from lesson 6.2). For any other op, print a complaint. Then call it a few times from main.

Show solution
fn calculate(x: i32, y: i32, op: char) {
    match op {
        '+' => println!("{}", x + y),
        '-' => println!("{}", x - y),
        '*' => println!("{}", x * y),
        '/' => println!("{}", x / y),
        '%' => println!("{}", x % y),
        _ => println!("calculate: unsupported operator '{op}'"),
    }
}

fn main() {
    calculate(7, 2, '+');
    calculate(7, 2, '/');
    calculate(7, 2, '%');
    calculate(7, 2, '?');
}
9
3
1
calculate: unsupported operator '?'

The wildcard arm earns its keep: without it, this match would be rejected as non-exhaustive (a char has over a million possible values, and we listed five).

Next: programs that refuse to stop. Loops, including the one C-family languages don't have.