Start learning free

LESSON 87 OF 414 · CONDITIONAL STATEMENTS

Match Expressions in Rust: Patterns, Ranges and Option

Lesson Read the full explanation

You already know how to branch with if / else if chains. Rust has a second branching tool that is usually the better one: match. It looks like switch from other languages, but it is stronger in two ways. First, match is an expression — the whole thing evaluates to a value, so you can bind its result directly with let. Second, the arms are patterns, not just single values: an inclusive range like 90..=100 matches every number from 90 to 100.

let temp = 3;
let advice = match temp {
    30..=45 => "stay in the shade",
    10..=29 => "perfect day",
    _ => "bring a jacket",
};
println!("{}", advice);   // prints: bring a jacket

Each arm reads pattern => value, separated by commas. Whichever arm matches first, its value becomes the value of the whole match.


The trap:

The compiler checks that your arms cover every possible value. Match an i32 with only two ranges, and the build stops with this error:

error[E0004]: non-exhaustive patterns: `i32::MIN..=79_i32` and `101_i32..=i32::MAX` not covered
  |
3 |     let grade = match score {
  |                       ^^^^^ patterns `i32::MIN..=79_i32` and `101_i32..=i32::MAX` not covered
  |
  = note: the matched value is of type `i32`
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

The compiler is telling you which values fell through the cracks: everything below 80 and above 100. The fix is a catch-all arm, written _ => ..., which matches anything the earlier arms didn't. This exhaustiveness check is a feature: you can never silently forget a case.


Matching an Option:

match really shines on values that may be missing. nums.get(1) looks up index 1 of a vector safely: it returns Some(value) if the index exists and None if it doesn't (this two-case type is called an Option — no crashes, no null). A match handles both cases in one place:

let msg = match nums.get(1) {
    Some(v) => format!("found {}", v),
    None => String::from("nothing"),
};

Some(v) doesn't just match — it unpacks the inner value into v so the arm can use it. One rule to remember: all arms must produce the same type. format! builds an owned String, so the None arm answers with String::from("nothing") rather than a bare text literal.


Assignment
Follow the Coding Tutorial to grade a score with range patterns, then look up a vector element with a match on an Option:

  • Declare score as 87 and match it into grade: 90..=100 is an "A", 80..=89 is a "B", anything else (_) is a "C". Print the grade.
  • Build nums as vec![1, 2, 3] and match on nums.get(1): on Some(v) build format!("found {}", v), on None use String::from("nothing"). Print the message.

The exact expected output is:

grade = B
msg = found 2

Hint
If the compiler says non-exhaustive patterns, you're missing the _ catch-all arm. If it complains about mismatched types in the arms, make sure both Option arms produce a String.


STEP 1 OF 10
    Ctrl + Enter to check

    RUST · CONDITIONAL STATEMENTS

    Match Expressions in Rust, explained

    What match is

    You already know how to branch with if and else if. Rust has a second tool, match, that is often the better choice when you compare one value against several possibilities. It resembles switch in other languages but is stronger in two ways:

    • match is an expression. The whole construct evaluates to a value, so you can bind it directly with let.
    • The arms are patterns, not only single values. A range such as 90..=100 matches every number from 90 up to and including 100.
    let temp = 3;
    let advice = match temp {
        30..=45 => "stay in the shade",
        10..=29 => "perfect day",
        _ => "bring a jacket",
    };
    println!("{}", advice);   // prints: bring a jacket
    

    Each arm has the form pattern => value, and arms are separated by commas. Rust tries the arms from top to bottom, and the value of the first arm that matches becomes the value of the whole match. The let statement ends with a semicolon after the closing brace, because let advice = match ... ; is one statement.

    Patterns you will use

    • A literal such as 3 matches exactly that value.
    • An inclusive range such as 10..=29 matches everything from 10 to 29.
    • Alternatives joined with |, such as 1 | 7, match either value.
    • A name binds the matched value for use in the arm.
    • The wildcard _ matches anything and binds nothing.

    The compiler wants every case covered

    A match must be exhaustive: its arms have to cover every possible value of the type. Match an i32 with only two ranges and the build stops:

    error[E0004]: non-exhaustive patterns: `i32::MIN..=79_i32` and `101_i32..=i32::MAX` not covered
    

    The message names the values that fall through: everything below 80 and everything above 100. The fix is a catch-all arm, _ => ..., which handles whatever the earlier arms did not. This check is a feature, not an obstacle. You can never forget a case silently, and when you later add a variant to an enum, the compiler points at every match that needs updating.

    Matching an Option

    A match is especially useful with values that may be missing. For a vector nums, the method nums.get(1) looks up index 1 safely. It returns Some(value) if the index exists and None if it does not. This two-case type is called Option. Indexing with nums[7] on a vector of three elements would panic, but nums.get(7) just gives None.

    let msg = match nums.get(1) {
        Some(v) => format!("found {}", v),
        None => String::from("nothing"),
    };
    

    The pattern Some(v) does more than test: it unpacks the inner value into v, which the arm can then use. All arms must produce the same type. format! returns an owned String, so the None arm uses String::from("nothing") as well. Writing a bare "nothing" there gives error[E0308]: `match` arms have incompatible types, because the other arm is a String and the literal is a &str.

    Common mistakes

    • Missing the catch-all. The non-exhaustive patterns error means some values are unhandled. Add _.
    • Putting _ first. Arms are tried in order, so a wildcard at the top catches everything and the arms below can never match.
    • Arms of different types. Every arm must produce the same type, as in the Option example above.
    • Off-by-one in ranges. 80..=89 includes 89, while 80..89 does not, and a range without = in a pattern leaves a gap.
    • Forgetting commas between arms, or the semicolon after the closing brace of a let.

    The lesson's exercise

    The exercise has two parts and prints two lines.

    1. Create score with the value 87.
    2. Write let grade = match score { ... }; so the chosen arm becomes the value of grade.
    3. Add the arms 90..=100 => "A" and 80..=89 => "B". The score 87 lands in the second one.
    4. Add _ => "C" so every other score is covered.
    5. Print grade = followed by the grade with println! and a {} placeholder.
    6. Build nums as vec![1, 2, 3].
    7. Match on nums.get(1) into msg, with a Some(v) arm that builds format!("found {}", v) and a None arm that gives String::from("nothing").
    8. Print msg = followed by the message.
    let score = 87;
    let grade = match score {
        90..=100 => "A",
        80..=89 => "B",
        _ => "C",
    };
    println!("grade = {}", grade);
    let nums = vec![1, 2, 3];
    let msg = match nums.get(1) {
        Some(v) => format!("found {}", v),
        None => String::from("nothing"),
    };
    println!("msg = {}", msg);
    

    The program prints:

    grade = B
    msg = found 2
    

    Indexes start at 0, so nums.get(1) is the second element, 2. Other scores give A for 90 and 100, B for 80 and 89, and C for 79 and 101. With nums.get(7) the second line would be msg = nothing.

    Key takeaways

    • match is an expression, so its result can be bound with let.
    • Arms are patterns, including inclusive ranges like 80..=89, tried from top to bottom.
    • The compiler requires every possible value to be covered; _ is the catch-all.
    • Use match on an Option to handle Some and None in one place, and make all arms the same type.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free