Skip to content
Codeloom
Rust

Pattern Matching in Rust

Master Rust's pattern matching — match expressions, if let, while let, destructuring, guards, and patterns in function parameters.

·4 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How match expressions work and why they must be exhaustive
  • Destructuring structs, enums, and tuples
  • if let and while let for concise matching
  • Match guards, bindings, and or-patterns

Prerequisites

  • Rust basics (variables, functions, enums)
  • Understanding of Option and Result types

Pattern matching is Rust’s most expressive control flow tool. The match expression forces you to handle every case, and destructuring lets you pull apart complex data in a single line.

The match expression

fn describe_number(n: i32) -> &'static str {
    match n {
        0 => "zero",
        1 => "one",
        2..=9 => "single digit",
        10..=99 => "double digit",
        _ => "large",
    }
}

Every possible value must be covered. The _ wildcard catches everything else. If you miss a case, the compiler tells you.

Matching enums

enum Direction {
    North,
    South,
    East,
    West,
}

fn move_player(dir: Direction) {
    match dir {
        Direction::North => println!("Moving north"),
        Direction::South => println!("Moving south"),
        Direction::East => println!("Moving east"),
        Direction::West => println!("Moving west"),
    }
}

Destructuring

Enums with data

enum Message {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
    Color(u8, u8, u8),
}

fn handle(msg: Message) {
    match msg {
        Message::Quit => println!("Quitting"),
        Message::Echo(text) => println!("Echo: {text}"),
        Message::Move { x, y } => println!("Move to ({x}, {y})"),
        Message::Color(r, g, b) => println!("Color: #{r:02x}{g:02x}{b:02x}"),
    }
}

Structs

struct Point {
    x: i32,
    y: i32,
}

let p = Point { x: 3, y: 7 };
let Point { x, y } = p;
println!("x={x}, y={y}");

Tuples

let (a, b, c) = (1, "hello", 3.14);

Nested destructuring

let ((a, b), Point { x, y }) = ((1, 2), Point { x: 3, y: 4 });

Option and Result

Pattern matching is the idiomatic way to handle Option and Result.

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some("Alice".to_string())
    } else {
        None
    }
}

match find_user(1) {
    Some(name) => println!("Found: {name}"),
    None => println!("Not found"),
}
match std::fs::read_to_string("config.toml") {
    Ok(contents) => println!("Config: {contents}"),
    Err(e) => eprintln!("Error: {e}"),
}

if let

When you only care about one variant, if let is more concise than a full match.

let value: Option<i32> = Some(42);

if let Some(n) = value {
    println!("Got: {n}");
}

Equivalent to:

match value {
    Some(n) => println!("Got: {n}"),
    _ => {}
}

while let

Loop while a pattern matches.

let mut stack = vec![1, 2, 3];

while let Some(top) = stack.pop() {
    println!("{top}");
}
// prints 3, 2, 1

Match guards

Add an if condition to a match arm.

let num = Some(4);

match num {
    Some(n) if n < 0 => println!("Negative: {n}"),
    Some(n) if n == 0 => println!("Zero"),
    Some(n) => println!("Positive: {n}"),
    None => println!("Nothing"),
}

Or-patterns

Match multiple patterns in one arm with |.

let x = 3;
match x {
    1 | 2 => println!("one or two"),
    3 | 4 => println!("three or four"),
    _ => println!("other"),
}

Bindings with @

Bind a value while also testing it against a pattern.

match age {
    n @ 0..=12 => println!("Child aged {n}"),
    n @ 13..=17 => println!("Teen aged {n}"),
    n @ 18.. => println!("Adult aged {n}"),
}

Ignoring values

Use _ to ignore a single value, .. to ignore remaining fields.

let (first, _, third) = (1, 2, 3); // ignore second

struct Point3D { x: f64, y: f64, z: f64 }
let Point3D { x, .. } = point; // only extract x

Patterns in function parameters

fn print_point(&(x, y): &(i32, i32)) {
    println!("({x}, {y})");
}

let point = (3, 5);
print_point(&point);

Summary

Pattern matching in Rust is exhaustive, expressive, and safe. Use match when you need to handle all cases, if let for one-variant checks, and destructuring to pull apart complex data. The compiler enforces that you handle every possibility — missing a case is a compile error, not a runtime bug.