Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Rust
  3. Rust Pattern Matching Practice: match, if let, Destructuring
Rust52 exercises

Rust Pattern Matching Practice: match, if let, Destructuring

Master Rust pattern matching including match expressions, if let, while let, and destructuring. Practice patterns for structs, enums, and tuples.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Write a match expression on x that returns "one" for 1, "two" for 2, and "other" for anything else.

On this page
  1. 1Compiler Error E0004: Non-Exhaustive Patterns
  2. 2Compiler Error E0005: Refutable Pattern in let Binding
  3. 3Compiler Error E0408: Variable Not Bound in All Alternatives
  4. 4Compiler Error E0507: Cannot Move Out of Borrowed Content
  5. 5Destructuring
  6. 6if let, while let, let else
  7. 7Patterns Are Everywhere
  8. 8Guards and @ Bindings
  9. Guards
  10. @ Bindings
  11. 9Combining Patterns with |
  12. 10Slice and Rest Patterns
  13. 11Further Reading
Compiler Error E0004: Non-Exhaustive PatternsCompiler Error E0005: Refutable Pattern in let BindingCompiler Error E0408: Variable Not Bound in All AlternativesCompiler Error E0507: Cannot Move Out of Borrowed ContentDestructuringif let, while let, let elsePatterns Are EverywhereGuards and @ BindingsCombining Patterns with |Slice and Rest PatternsFurther Reading

Pattern matching is one of Rust's most powerful features. The match expression lets you compare a value against patterns and run code based on which matches. It's exhaustive: you must handle every possibility.

Beyond match, you'll use if let for single patterns, while let for loops, let else for bind-or-return, and destructuring everywhere. Patterns work on tuples, structs, enums, slices, and references.

This page focuses on pattern syntax until matching becomes natural. From basic match arms to complex destructuring with guards and slice patterns.

Related Rust Topics
Rust Structs & Enums: Custom Types, Option & ResultRust Error Handling: Result, Option, ? Operator

match must cover every possible value. If you add a variant to an enum, the compiler flags every incomplete match. Use _ as a catch-all only when you genuinely don't care about future variants.

The most common pattern matching error. Your match doesn't cover every variant:

// WRONG: missing None arm
fn describe(optional: Option<i32>) -> &'static str {
    match optional {
        Some(x) if x > 0 => "positive",
        Some(_) => "non-positive",
        // error[E0004]: non-exhaustive patterns: `None` not covered
    }
}

// RIGHT: handle all cases
fn describe(optional: Option<i32>) -> &'static str {
    match optional {
        Some(x) if x > 0 => "positive",
        Some(_) => "non-positive",
        None => "nothing",
    }
}

The compiler enforces exhaustiveness because unhandled cases are runtime bugs in other languages. In Rust, they're compile-time errors. If you add a variant to an enum, the compiler flags every match that needs updating. See structs and enums for how enum variants carry data that you destructure in match arms.

Ready to practice?

Start practicing Rust Pattern Matching: match, if let, Destructuring with spaced repetition

let bindings require irrefutable patterns — patterns that always match:

// WRONG: Some(x) might not match
// let Some(x) = some_option;
// error[E0005]: refutable pattern in local binding

// RIGHT Option 1: if let for "do something if it matches"
if let Some(x) = some_option {
    println!("{x}");
}

// RIGHT Option 2: let else for "bind or return early"
let Some(x) = some_option else {
    return;  // must diverge: return, break, continue, or panic!
};
println!("{x}");  // x is in scope here

let else is the "bind-or-return" pattern — it unwraps or exits the current block. The else branch must diverge (it cannot fall through).

With | patterns, every alternative must bind the same names:

// WRONG: x is bound on the left but not the right
// match pair {
//     (x, 0) | (0, _) => println!("{x}"),  // error[E0408]
//     _ => {}
// }

// RIGHT: bind x on both sides
match pair {
    (x, 0) | (0, x) => println!("{x}"),
    (a, b) => println!("({a}, {b})"),
}

Matching by value on borrowed data tries to move, which the borrow checker rejects:

// WRONG: matching by value moves out of the reference
// fn first_name(name: &Option<String>) -> String {
//     match name {
//         Some(s) => s.clone(),  // error[E0507]: cannot move out of *name
//         None => String::new(),
//     }
// }

// RIGHT: match on the reference — bind by reference
fn first_name(name: &Option<String>) -> String {
    match name {
        Some(s) => s.clone(),   // s is &String here (match ergonomics)
        None => String::new(),
    }
}
// Or be explicit:
fn first_name_explicit(name: &Option<String>) -> String {
    match *name {
        Some(ref s) => s.clone(),  // ref avoids the move
        None => String::new(),
    }
}

When matching on a reference, Rust automatically adjusts binding modes so Some(s) binds s as a reference. This is called match ergonomics. For a copy-ready reference of all pattern syntax, see the pattern matching cheat sheet.

Patterns destructure everywhere: let, match, function parameters, for loops, closures. Use _ to ignore single values and .. to ignore the rest.

Pull values out of structs, tuples, and enums directly in the pattern:

// Struct destructuring
let Point { x, y } = point;

// Tuple destructuring (use _ to ignore)
let (first, second, _) = tuple;

// Enum destructuring in match
match msg {
    Message::Move { x, y } => println!("move to ({x}, {y})"),
    Message::Write(text) => println!("{text}"),
    Message::Quit => println!("quit"),
}

// Nested destructuring
let ((a, b), Point { x, .. }) = (pair, point);

if let handles one pattern (skip the rest). while let loops until the pattern stops matching. let else binds or diverges (return/break/panic). Use these instead of match when you only care about one case.

Patterns are not limited to match:

// if let: handle one pattern, skip the rest
if let Some(value) = optional {
    println!("{value}");
}

// while let: loop until the pattern stops matching
while let Some(value) = stack.pop() {
    println!("{value}");
}

// let else: bind or return early (must diverge)
let Ok(port) = env::var("PORT")
    .map(|v| v.parse::<u16>())
    .unwrap_or(Ok(8080)) else {
    eprintln!("invalid PORT");
    std::process::exit(1);
};

Patterns appear in more places than just match and if let:

// let bindings
let (x, y) = (1, 2);

// Function parameters
fn print_point(&Point { x, y }: &Point) {
    println!("({x}, {y})");
}

// for loops
for (i, value) in vec.iter().enumerate() {
    println!("{i}: {value}");
}

// Closure parameters
let sum = |(a, b): (i32, i32)| a + b;

Guards

Add conditions after a pattern with if. Guards don't affect exhaustiveness checking — the compiler still requires the patterns themselves to cover all possibilities:

match temperature {
    t if t > 100 => "boiling",
    t if t < 0 => "freezing",
    _ => "normal",  // required: guards don't count as exhaustive
}

Precedence gotcha: a guard applies to the entire | pattern, not just the last alternative:

match x {
    // guard applies to BOTH 4 and 5, not just 5
    4 | 5 if condition => "matched",
    _ => "other",
}

@ Bindings

Bind a value to a name while simultaneously testing it against a pattern:

match msg {
    Message::Hello { id: id @ 3..=7 } => println!("id in range: {id}"),
    Message::Hello { id } => println!("other id: {id}"),
    _ => (),
}

// @ with ranges
match age {
    n @ 0..=12 => println!("child aged {n}"),
    n @ 13..=19 => println!("teenager aged {n}"),
    n => println!("adult aged {n}"),
}

Match multiple patterns in a single arm:

match x {
    1 | 2 | 3 => println!("small"),
    _ => println!("big"),
}

Every alternative in a | pattern must bind the same variable names (E0408).

Match on slices with [] patterns. Use .. for the rest (only once per pattern):

match slice {
    [] => println!("empty"),
    [one] => println!("single: {one}"),
    [first, .., last] => println!("first: {first}, last: {last}"),
}

// Bind the tail with @
match slice {
    [head, tail @ ..] => println!("head: {head}, tail has {} elements", tail.len()),
    [] => println!("empty"),
}

// Bind the whole slice and destructure
match slice {
    whole @ [.., last] => println!("last of {}: {last}", whole.len()),
    [] => println!("empty"),
}

Only one .. is allowed per pattern — two would be ambiguous about which elements to skip. Exhaustive pattern matching on Result and Option is central to Rust's error handling approach, where the ? operator provides a concise alternative to match for propagating errors. Pattern matching also enables the Strategy design pattern, where different match arms dispatch to different behaviors.

  • The Rust Book: Patterns and Matching — exhaustive treatment of all pattern types
  • The Rust Book: Pattern Syntax — guards, @, |, .., and nested patterns
  • Rust Reference: Patterns — formal grammar, all pattern kinds
  • Rust By Example: match — practical examples with destructuring

When to Use Rust Pattern Matching: match, if let, Destructuring

  • Use `match` when you need to handle multiple cases exhaustively.
  • Use `if let` when you only care about one pattern and want to ignore the rest.
  • Use `let else` for bind-or-return-early patterns.
  • Use `while let` to loop while a pattern matches.
  • Use destructuring to extract data from complex types.
  • Use guards (`if condition`) for additional constraints on match arms.

Check Your Understanding: Rust Pattern Matching: match, if let, Destructuring

Prompt

Why is match exhaustive and why does that matter?

What a strong answer looks like

Match forces you to handle every possible value of the type. This prevents bugs from forgotten cases. If you add a variant to an enum, the compiler shows every match that needs updating.

What You'll Practice: Rust Pattern Matching: match, if let, Destructuring

Write exhaustive match expressionsUse if let, while let, and let elseMatch on literals, ranges, and multiple patterns (|)Destructure tuples, structs, enums, and slicesAdd match guards (if conditions)Use @ bindings for pattern + valueUse slice patterns with [head, tail @ ..]Understand match ergonomics (reference binding)Fix E0004, E0005, E0408, E0507

Common Rust Pattern Matching: match, if let, Destructuring Pitfalls

  • You get `error[E0004]: non-exhaustive patterns` -- you forgot a variant in your `match`. Add the missing arm or use `_ => ...` as a catch-all. Adding a variant to an enum will flag every incomplete match.
  • You get `error[E0005]: refutable pattern in local binding` -- you used `let Some(x) = ...` but the value might be `None`. Switch to `if let Some(x) = ...` or `let Some(x) = ... else { return; };`.
  • You get `error[E0408]: variable is not bound in all patterns` -- you wrote `A(x) | B` with `|` but only one side binds `x`. Every alternative must bind the same variable names.
  • You get `error[E0507]: cannot move out of borrowed content` -- matching by value on borrowed data tries to move. Match on the reference directly (match ergonomics) or use `ref` to bind by reference.
  • Your catch-all arm `_` runs when you expected a specific arm to match -- arms are checked top to bottom. A broad pattern above shadows specific ones below. Move specific patterns first.
  • A guard on a `|` pattern behaves unexpectedly -- the guard applies to the entire `|` expression, not just the last alternative. `4 | 5 if cond` means `(4 | 5) if cond`, not `4 | (5 if cond)`.

Rust Pattern Matching: match, if let, Destructuring FAQ

What is the difference between match and if let?

`match` is exhaustive and you handle every case. `if let` handles one pattern and ignores the rest. Use `if let` when you only care about one variant: `if let Some(x) = option { }`.

How do I match multiple patterns in one arm?

Use the `|` operator: `1 | 2 | 3 => println!("one to three")`. This matches any of the listed patterns.

What are match guards?

Extra conditions on match arms: `Some(x) if x > 5 => ...`. The arm only matches if both the pattern and guard are true.

How do I ignore parts of a pattern?

Use `_` for values you do not need: `(x, _)` ignores the second element. Use `..` to ignore multiple: `Point { x, .. }` ignores y and z.

Can I bind a value and match a pattern at the same time?

Use `@` for binding: `Some(x @ 1..=5) => println!("got {x}")`. This both matches the range and binds the value to `x`.

What is let else?

`let PATTERN = expr else { diverge };` binds if the pattern matches, otherwise executes the else block which must diverge (return, break, continue, or panic). It replaces the common "match or return" pattern.

What is match ergonomics?

When matching on a reference, Rust automatically adjusts binding modes so `Some(x)` binds `x` as a reference instead of trying to move. This avoids needing `ref` in most cases.

Rust Pattern Matching: match, if let, Destructuring Syntax Quick Reference

Basic match
match x {
    1 => println!("one"),
    2 => println!("two"),
    _ => println!("other"),
}
Match on Option
match optional {
    Some(value) => println!("got {value}"),
    None => println!("nothing"),
}
Multiple patterns
match x {
    1 | 2 | 3 => println!("small"),
    _ => println!("big"),
}
Range pattern
match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}
Match guard
match x {
    Some(n) if n > 5 => println!("big"),
    Some(n) => println!("small: {n}"),
    None => println!("none"),
}
if let
if let Some(value) = optional {
    println!("got {value}");
}
while let
while let Some(value) = stack.pop() {
    println!("{value}");
}
let else
let Some(x) = optional else {
    return;
};
Slice pattern
match slice {
    [first, .., last] => println!("{first} {last}"),
    [] => println!("empty"),
}
Destructure struct
let Point { x, y } = point;
Destructure tuple
let (first, second, _) = tuple;
@ binding
match msg {
    Message::Hello { id: id @ 3..=7 } => println!("id in range: {id}"),
    _ => (),
}

Rust Pattern Matching: match, if let, Destructuring Sample Exercises

Example 1Difficulty: 2/5

Fill in the match arm to return "zero" when n is 0.

"zero"
Example 2Difficulty: 1/5

Fill in the keyword to start a match expression.

match
Example 3Difficulty: 2/5

Fill in the pattern to handle the empty case.

None

+ 49 more exercises

Quick Reference
Rust Pattern Matching: match, if let, Destructuring Cheat Sheet →

Copy-ready syntax examples for quick lookup

Further Reading

  • Rust Newtype Pattern: Catch Unit Bugs at Compile Time18 min read

Related Design Patterns

Strategy Pattern

Start practicing Rust Pattern Matching: match, if let, Destructuring

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.