ZetCode

Rust pattern matching

last modified November 13, 2026

In this article we look at pattern matching in Rust.

A pattern describes the shape of a value. Matching a value against a pattern does two things: it checks whether the value has that shape, and it binds parts of the value to variables. Patterns are made of literals, ranges, wildcards, variable names, and the structure of tuples, structs, enums, slices, and references.

Patterns appear in match arms, but also in let statements, function parameters, for loops, and in the if let, let else, and while let constructs. The basics of match are covered in Rust match expression; this article focuses on the other forms and on the pattern syntax. The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q.

Where patterns appear

Many constructs that do not look like pattern matching take a pattern.

main.rs
struct Point {
    x: i32,
    y: i32,
}

fn shift(Point { x, y }: Point, dx: i32) -> Point {
    Point { x: x + dx, y }
}

fn main() {
    let (a, b) = (1, 2);
    println!("let: {a} {b}");

    let Point { x, y } = shift(Point { x: 1, y: 5 }, 10);
    println!("parameter: {x} {y}");

    for (i, c) in "abc".chars().enumerate() {
        print!("{i}{c} ");
    }
    println!();

    let add = |(p, q): (i32, i32)| p + q;
    println!("closure: {}", add((3, 4)));

    let n = a + b;
    let kind = match n {
        0 => "zero",
        1 | 2 => "small",
        _ => "large",
    };
    println!("match: {kind}");
}

Each of these statements uses a pattern.

let (a, b) = (1, 2);

The left side of let is a pattern. A plain variable name is the simplest pattern; it matches anything and binds it.

fn shift(Point { x, y }: Point, dx: i32) -> Point {

Function parameters are patterns. Here the struct is destructured right in the parameter list.

for (i, c) in "abc".chars().enumerate() {

The variable of a for loop is a pattern, which destructures each tuple produced by the iterator. Closure parameters work the same way.

$ cargo run -q
let: 1 2
parameter: 11 5
0a 1b 2c 
closure: 7
match: large

Refutable and irrefutable patterns

A pattern that matches every possible value is irrefutable; a pattern that can fail is refutable. Plain let, function parameters, and for loops accept only irrefutable patterns, because there is nothing they could do when the match fails.

main.rs
fn main() {
    let input = "42";
    let Ok(n) = input.parse::<i32>();
    println!("{n}");
}

The parse method returns a Result, and the Ok(n) pattern does not cover the Err case.

let Ok(n) = input.parse::<i32>();

The compiler rejects the statement with error E0005, even though the string happens to be a valid number. It suggests let...else, which we cover below.

$ cargo run -q
error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Ok(n) = input.parse::<i32>();
  |         ^^^^^ pattern `Err(_)` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `std::result::Result<i32, std::num::ParseIntError>`
help: you might want to use `let...else` to handle the variant that isn't matched
  |
3 |     let Ok(n) = input.parse::<i32>() else { todo!() };
  |                                      ++++++++++++++++

For more information about this error, try `rustc --explain E0005`.

The if let expression

The if let expression runs a block when a refutable pattern matches. It is a shorter form of a match with one interesting arm.

main.rs
use std::collections::HashMap;

fn main() {
    let mut stock = HashMap::new();
    stock.insert("apples", 12);
    stock.insert("pears", 0);

    for item in ["apples", "pears", "plums"] {
        if let Some(&count) = stock.get(item) {
            println!("{item}: {count}");
        } else {
            println!("{item}: unknown");
        }
    }

    let values = ["7", "x", "-3"];
    for v in values {
        if let Ok(n) = v.parse::<i32>() && n > 0 {
            println!("{v} is a positive number");
        } else {
            println!("{v} is not a positive number");
        }
    }
}

We look up items in a map and parse strings.

if let Some(&count) = stock.get(item) {
    println!("{item}: {count}");
} else {
    println!("{item}: unknown");
}

The get method returns Option<&i32>. The Some(&count) pattern matches a present value and copies the number out of the reference. The else branch handles None.

if let Ok(n) = v.parse::<i32>() && n > 0 {

In the 2024 edition, if let can be combined with ordinary conditions using &&. The variable n bound by the pattern is available in the condition that follows.

$ cargo run -q
apples: 12
pears: 0
plums: unknown
7 is a positive number
x is not a positive number
-3 is not a positive number

Let chains

A let chain joins several let patterns and boolean conditions with &&. Before let chains, such code needed nested if let blocks.

main.rs
struct Config {
    port: Option<String>,
    debug: Option<String>,
}

fn describe(cfg: &Config) {
    if let Some(p) = &cfg.port
        && let Ok(port) = p.parse::<u16>()
        && port >= 1024
        && let Some(d) = &cfg.debug
        && d == "on"
    {
        println!("debug server on port {port}");
    } else {
        println!("normal start");
    }
}

fn main() {
    let configs = [
        Config { port: Some("8080".into()), debug: Some("on".into()) },
        Config { port: Some("80".into()), debug: Some("on".into()) },
        Config { port: Some("abc".into()), debug: None },
        Config { port: None, debug: Some("on".into()) },
    ];

    for c in &configs {
        describe(c);
    }
}

The server starts in debug mode only when the port is present, valid, not privileged, and the debug option is on.

if let Some(p) = &cfg.port
    && let Ok(port) = p.parse::<u16>()
    && port >= 1024
    && let Some(d) = &cfg.debug
    && d == "on"
{

The conditions are evaluated from left to right, and each one can use the bindings of the previous ones. The first failing condition stops the chain and the else branch runs. Let chains are available since Rust 1.88 in the 2024 edition; || cannot be used between let patterns.

$ cargo run -q
debug server on port 8080
normal start
normal start
normal start

The let else statement

The let else statement binds variables from a refutable pattern, or runs an else block when it does not match. The else block must diverge: it has to return, break, continue, or panic.

main.rs
fn parse_pair(line: &str) -> Result<(String, u32), String> {
    let Some((name, age)) = line.split_once(',') else {
        return Err(format!("missing comma: {line:?}"));
    };

    let Ok(age) = age.trim().parse::<u32>() else {
        return Err(format!("bad age: {age:?}"));
    };

    Ok((name.trim().to_string(), age))
}

fn main() {
    let lines = ["Anna, 31", "Peter 45", "Lucia, old", "Bob,27"];

    for line in lines {
        match parse_pair(line) {
            Ok((name, age)) => println!("{name} is {age}"),
            Err(e) => println!("error: {e}"),
        }
    }
}

The function parses lines of the form name, age.

let Some((name, age)) = line.split_once(',') else {
    return Err(format!("missing comma: {line:?}"));
};

If the line contains a comma, name and age are bound for the rest of the function. Otherwise the function returns early.

let Ok(age) = age.trim().parse::<u32>() else {
    return Err(format!("bad age: {age:?}"));
};

The new age shadows the string slice. Inside the else block, the old age is still the string, so it can be shown in the error message. This style keeps the happy path unindented, unlike nested if let blocks.

$ cargo run -q
Anna is 31
error: missing comma: "Peter 45"
error: bad age: " old"
Bob is 27

The while let loop

The while let loop runs as long as a pattern matches.

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

    let text = "a1b22c333";
    let mut chars = text.chars().peekable();
    while let Some(c) = chars.next() {
        let mut digits = String::new();
        while let Some(d) = chars.next_if(|ch| ch.is_ascii_digit()) {
            digits.push(d);
        }
        println!("{c} -> {digits}");
    }

    let mut queue = vec![Some(5), Some(8), None, Some(1)];
    queue.reverse();
    while let Some(Some(job)) = queue.pop() {
        println!("job {job}");
    }
    println!("left: {queue:?}");
}

We use it with a stack, a peekable iterator, and nested options.

while let Some(top) = stack.pop() {

The pop method returns None when the vector is empty, which ends the loop.

while let Some(d) = chars.next_if(|ch| ch.is_ascii_digit()) {

The next_if method consumes the next character only if it satisfies the predicate. The inner loop collects the digits following each letter.

while let Some(Some(job)) = queue.pop() {

The pattern can be nested. The loop stops at the first None job; that element has been popped, but the rest of the queue stays.

$ cargo run -q
3 2 1 
a -> 1
b -> 22
c -> 333
job 5
job 8
left: [Some(1)]

Literal and range patterns

Literals match exact values. Range patterns match a range of numbers or characters.

main.rs
fn classify(c: char) -> &'static str {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' => "vowel",
        'a'..='z' => "consonant",
        'A'..='Z' => "uppercase",
        '0'..='9' => "digit",
        _ => "other",
    }
}

fn grade(score: u32) -> char {
    match score {
        90.. => 'A',
        80..90 => 'B',
        70..80 => 'C',
        60..70 => 'D',
        ..60 => 'F',
    }
}

fn main() {
    for c in ['e', 'k', 'Q', '7', '#'] {
        println!("{c}: {}", classify(c));
    }

    let scores = [95, 83, 70, 61, 12];
    let grades: String = scores.iter().map(|&s| grade(s)).collect();
    println!("{grades}");

    let temp = -3.5_f64;
    let desc = match temp {
        t if t < 0.0 => "freezing",
        0.0..15.0 => "cold",
        15.0..25.0 => "pleasant",
        _ => "hot",
    };
    println!("{desc}");
}

We classify characters and convert scores to grades.

'a' | 'e' | 'i' | 'o' | 'u' => "vowel",
'a'..='z' => "consonant",

The | operator combines alternatives. The vowels also fall into the 'a'..='z' range, but the first matching arm wins.

90.. => 'A',
80..90 => 'B',
...
..60 => 'F',

Exclusive ranges (80..90) and half-open ranges (90.., ..60) are allowed in patterns. The compiler checks that the ranges cover every u32, so no wildcard arm is needed.

t if t < 0.0 => "freezing",
0.0..15.0 => "cold",

Floats can be matched against ranges too. The compiler cannot verify that float ranges are exhaustive, so a final _ arm is required.

$ cargo run -q
e: vowel
k: consonant
Q: uppercase
7: digit
#: other
ABCDF
freezing

Match guards

A match guard is an additional if condition after a pattern. The arm is chosen only when both the pattern matches and the guard is true.

main.rs
fn describe(pair: (i32, i32)) -> String {
    match pair {
        (x, y) if x == y => format!("equal: {x}"),
        (x, y) if x + y == 0 => format!("opposites: {x} {y}"),
        (x, _) if x % 2 == 0 => format!("first is even: {x}"),
        _ => "no match".to_string(),
    }
}

fn main() {
    for p in [(4, 4), (3, -3), (6, 1), (5, 2)] {
        println!("{}", describe(p));
    }

    let limit = 10;
    for v in [Some(3), Some(12), None] {
        match v {
            Some(n) if n > limit => println!("{n} is over the limit"),
            Some(n) => println!("{n} is fine"),
            None => println!("no value"),
        }
    }

    let x = 4;
    let flag = false;
    match x {
        2 | 4 | 9 if flag => println!("yes"),
        _ => println!("no"),
    }
}

Guards can compare the bound variables with each other and with other variables.

(x, y) if x == y => format!("equal: {x}"),

A pattern cannot express that two parts of a value are equal; a guard can. Writing (x, x) is a compile error.

Some(n) if n > limit => println!("{n} is over the limit"),

A name in a pattern always creates a new binding; it is never compared with an existing variable. To compare with limit, we need a guard.

2 | 4 | 9 if flag => println!("yes"),

A guard applies to all alternatives of the pattern, not only to the last one. The arm means (2 | 4 | 9) if flag.

$ cargo run -q
equal: 4
opposites: 3 -3
first is even: 6
no match
3 is fine
12 is over the limit
no value
no

Bindings with @

The @ operator binds a value to a name while also testing it against a pattern.

main.rs
#[derive(Debug)]
enum Message {
    Move { x: i32, y: i32 },
    Write(String),
    Quit,
}

fn age_group(age: u32) -> String {
    match age {
        n @ 0..=12 => format!("child ({n})"),
        n @ 13..=19 => format!("teenager ({n})"),
        n => format!("adult ({n})"),
    }
}

fn main() {
    for age in [7, 16, 42] {
        println!("{}", age_group(age));
    }

    let msgs = [
        Message::Move { x: 3, y: 0 },
        Message::Move { x: 1, y: 7 },
        Message::Write("hello".into()),
        Message::Quit,
    ];

    for m in &msgs {
        match m {
            Message::Move { x, y: y @ 1.. } => println!("move up by {y}, x={x}"),
            mv @ Message::Move { .. } => println!("other move: {mv:?}"),
            Message::Write(text) if text.len() < 10 => println!("short text {text}"),
            other => println!("{other:?}"),
        }
    }

    let v = vec![1, 2, 3, 4, 5];
    if let [first, rest @ ..] = v.as_slice() {
        println!("{first} then {rest:?}");
    }
}

We bind numbers, struct fields, whole enum values, and slice rests.

n @ 0..=12 => format!("child ({n})"),

The value is checked against the range and, if it matches, bound to n. Without @, we would know the range but not the value.

Message::Move { x, y: y @ 1.. } => println!("move up by {y}, x={x}"),
mv @ Message::Move { .. } => println!("other move: {mv:?}"),

In the first arm, the y field must be positive. The second arm binds the whole message to mv.

if let [first, rest @ ..] = v.as_slice() {

In slice patterns, rest @ .. binds the remaining elements as a subslice.

$ cargo run -q
child (7)
teenager (16)
adult (42)
other move: Move { x: 3, y: 0 }
move up by 7, x=1
short text hello
Quit
1 then [2, 3, 4, 5]

Struct and enum patterns

Struct patterns name the fields to match. They can be nested to look deep into a value.

main.rs
#[derive(Debug)]
struct Address {
    city: String,
    country: String,
}

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    address: Option<Address>,
}

fn main() {
    let users = vec![
        User {
            name: "Anna".into(),
            age: 31,
            address: Some(Address { city: "Bratislava".into(), country: "SK".into() }),
        },
        User { name: "Peter".into(), age: 17, address: None },
        User {
            name: "Lucia".into(),
            age: 45,
            address: Some(Address { city: "Vienna".into(), country: "AT".into() }),
        },
    ];

    for user in &users {
        match user {
            User { name, address: Some(Address { country, .. }), .. } if country == "SK" => {
                println!("{name} lives in Slovakia")
            }
            User { name, age: 0..18, .. } => println!("{name} is a minor"),
            User { name, address: Some(Address { city, .. }), .. } => {
                println!("{name} lives in {city}")
            }
            User { name, .. } => println!("{name}: no address"),
        }
    }

    let User { name, age, .. } = &users[2];
    println!("{name} {age}");
}

We sort users into categories based on their age and address.

User { name, address: Some(Address { country, .. }), .. } if country == "SK" => {

The pattern reaches through the Option into the nested Address. A field name alone, such as name, is a shorthand for name: name. The .. skips the remaining fields.

User { name, age: 0..18, .. } => println!("{name} is a minor"),

Field patterns can be literals or ranges, not just names.

let User { name, age, .. } = &users[2];

Struct patterns are irrefutable, so they can be used with let. Destructuring a reference binds references to the fields.

$ cargo run -q
Anna lives in Slovakia
Peter is a minor
Lucia lives in Vienna
Lucia 45

Slice patterns

Slice patterns match slices and arrays by their length and elements.

main.rs
fn describe(v: &[i32]) -> String {
    match v {
        [] => "empty".to_string(),
        [x] => format!("one element: {x}"),
        [first, second] => format!("pair: {first} {second}"),
        [first, .., last] => format!("{} elements, {first}..{last}", v.len()),
    }
}

fn sum(v: &[i32]) -> i32 {
    match v {
        [] => 0,
        [head, tail @ ..] => head + sum(tail),
    }
}

fn command(args: &[&str]) -> String {
    match args {
        ["add", name] => format!("adding {name}"),
        ["remove", names @ ..] if !names.is_empty() => format!("removing {}", names.join(", ")),
        ["list"] | [] => "listing".to_string(),
        [cmd, ..] => format!("unknown command {cmd}"),
    }
}

fn main() {
    for v in [&[][..], &[5], &[1, 2], &[3, 6, 9, 12]] {
        println!("{}", describe(v));
    }

    println!("sum: {}", sum(&[1, 2, 3, 4]));

    for args in [&["add", "milk"][..], &["remove", "eggs", "salt"], &[], &["fly"]] {
        println!("{}", command(args));
    }

    let arr = [10, 20, 30, 40];
    let [a, b, rest @ ..] = arr;
    println!("{a} {b} {rest:?}");
}

We describe slices of various lengths, sum a slice recursively, and parse commands.

[] => "empty".to_string(),
[x] => format!("one element: {x}"),
[first, second] => format!("pair: {first} {second}"),
[first, .., last] => format!("{} elements, {first}..{last}", v.len()),

Patterns without .. match an exact length. The last arm matches any slice with at least two elements; together the arms cover all lengths.

[head, tail @ ..] => head + sum(tail),

Splitting a slice into its head and tail is a natural fit for recursion.

["add", name] => format!("adding {name}"),
["remove", names @ ..] if !names.is_empty() => ...

Elements can be matched against string literals. This makes slice patterns handy for simple command-line parsing.

let [a, b, rest @ ..] = arr;

With an array, the length is known at compile time, so this pattern is irrefutable and works with let. The rest is an array of two elements.

$ cargo run -q
empty
one element: 5
pair: 1 2
4 elements, 3..12
sum: 10
adding milk
removing eggs, salt
listing
unknown command fly
10 20 [30, 40]

Matching references

When a reference is matched against a non-reference pattern, Rust automatically dereferences it and binds the variables as references. This is known as default binding modes or match ergonomics.

main.rs
fn main() {
    let names = vec![Some(String::from("Anna")), None, Some(String::from("Bob"))];

    for item in &names {
        match item {
            Some(name) => println!("{name} has {} letters", name.len()),
            None => println!("missing"),
        }
    }
    println!("{names:?}");

    let mut counts = [Some(1), None, Some(5)];
    for c in &mut counts {
        match c {
            Some(n) => *n *= 10,
            None => *c = Some(0),
        }
    }
    println!("{counts:?}");

    let nums = [1, -2, 3, -4];
    let pos: Vec<i32> = nums.iter().filter(|&&n| n > 0).copied().collect();
    println!("{pos:?}");

    let pair = (String::from("key"), 7);
    match pair {
        (ref k, n) if n > 5 => println!("{k} is big"),
        _ => println!("small"),
    }
    println!("still usable: {pair:?}");
}

We match through shared and mutable references.

for item in &names {
    match item {
        Some(name) => println!("{name} has {} letters", name.len()),

The item is &Option<String>, so name becomes &String. Nothing is moved and the vector can be printed afterwards.

for c in &mut counts {
    match c {
        Some(n) => *n *= 10,
        None => *c = Some(0),
    }
}

Matching a mutable reference binds n as &mut i32, which lets us change the value in place.

nums.iter().filter(|&&n| n > 0)

The filter closure receives &&i32. The &&n pattern strips both references and copies the number.

(ref k, n) if n > 5 => println!("{k} is big"),

When matching an owned value, the ref keyword binds a reference instead of moving the field. Without it, the string would be moved into k and pair could not be printed afterwards.

$ cargo run -q
Anna has 4 letters
missing
Bob has 3 letters
[Some("Anna"), None, Some("Bob")]
[Some(10), Some(0), Some(50)]
[1, 3]
key is big
still usable: ("key", 7)

Matching strings

String literals in patterns have the type &str. To match a String, we convert it to a string slice first.

main.rs
fn respond(cmd: &str) -> &'static str {
    match cmd.trim().to_lowercase().as_str() {
        "hi" | "hello" => "Hello there!",
        "bye" => "Goodbye!",
        s if s.starts_with("help") => "Commands: hi, bye, help",
        "" => "Say something.",
        _ => "I do not understand.",
    }
}

fn main() {
    for input in ["Hello", "  BYE ", "help me", "", "dance"] {
        println!("{input:?} -> {}", respond(input));
    }

    let owned = String::from("n");
    let answer = match owned.as_str() {
        "yes" | "y" => "accepted",
        "no" | "n" => "rejected",
        _ => "unclear",
    };
    println!("{answer}");
}

A simple command responder normalizes its input before matching.

match cmd.trim().to_lowercase().as_str() {

The to_lowercase method returns a new String; as_str turns it into &str, which can be compared with literal patterns.

s if s.starts_with("help") => "Commands: hi, bye, help",

Patterns cannot match a prefix of a string. A binding with a guard does the job.

$ cargo run -q
"Hello" -> Hello there!
"  BYE " -> Goodbye!
"help me" -> Commands: hi, bye, help
"" -> Say something.
"dance" -> I do not understand.
rejected

The matches! macro

The matches! macro tests whether a value matches a pattern and returns a bool.

main.rs
#[derive(Debug)]
enum Token {
    Num(f64),
    Op(char),
    LParen,
    RParen,
}

fn main() {
    let tokens = [
        Token::LParen,
        Token::Num(2.0),
        Token::Op('+'),
        Token::Num(3.5),
        Token::RParen,
        Token::Op('*'),
        Token::Num(4.0),
    ];

    let numbers = tokens.iter().filter(|t| matches!(t, Token::Num(_))).count();
    let ops: Vec<char> = tokens
        .iter()
        .filter_map(|t| match t {
            Token::Op(c) => Some(*c),
            _ => None,
        })
        .collect();
    let parens = tokens.iter().filter(|t| matches!(t, Token::LParen | Token::RParen)).count();
    let big = tokens.iter().any(|t| matches!(t, Token::Num(n) if *n > 3.0));

    println!("numbers: {numbers}, ops: {ops:?}, parens: {parens}, big: {big}");

    let c = 'k';
    println!("{}", matches!(c, 'a'..='z' | '0'..='9' | '_'));
    println!("{:?}", tokens.last());
}

We count and inspect tokens of a small expression.

tokens.iter().filter(|t| matches!(t, Token::Num(_))).count();

The macro is handy in closures passed to filter, any, or all, where a full match would be verbose.

tokens.iter().any(|t| matches!(t, Token::Num(n) if *n > 3.0));

The pattern can have alternatives and a guard. Bindings such as n are visible only inside the guard.

$ cargo run -q
numbers: 3, ops: ['+', '*'], parens: 2, big: true
true
Some(Num(4.0))

Or-patterns

The | operator can be used anywhere inside a pattern, not only at the top level of a match arm.

main.rs
enum Shape {
    Circle(f64),
    Square(f64),
    Rect(f64, f64),
}

fn main() {
    let shapes = [Shape::Circle(1.0), Shape::Square(2.0), Shape::Rect(2.5, 3.0)];

    for s in &shapes {
        let (Shape::Circle(width) | Shape::Square(width) | Shape::Rect(width, _)) = s;
        let tall = matches!(s, Shape::Rect(w, h) if h > w);
        println!("width: {width}, tall: {tall}");
    }

    for v in [Some(1), Some(4), Some(7), None] {
        match v {
            Some(1 | 3 | 5) => println!("{v:?} is small and odd"),
            Some(n) if n < 6 => println!("{v:?} is small and even"),
            Some(_) => println!("{v:?} is large"),
            None => println!("nothing"),
        }
    }

    let res: Result<u32, u32> = Err(3);
    let (Ok(n) | Err(n)) = res;
    println!("{n}");
}

We use or-patterns in let, nested in Some, and across variants.

let (Shape::Circle(width) | Shape::Square(width) | Shape::Rect(width, _)) = s;

All alternatives bind the same variable with the same type. Together they cover every variant, so the pattern is irrefutable and works in a let statement. Parentheses are required around a top-level or-pattern in let.

Some(1 | 3 | 5) => println!("{v:?} is small and odd"),

Nested alternatives avoid repeating Some in each case.

let (Ok(n) | Err(n)) = res;

When both variants of a Result hold the same type, this extracts the value regardless of the variant.

$ cargo run -q
width: 1, tall: false
width: 2, tall: false
width: 2.5, tall: true
Some(1) is small and odd
Some(4) is small and even
Some(7) is large
nothing
3

Exhaustiveness checking

A match must cover every possible value. The compiler checks this and reports the missing cases.

main.rs
enum Direction {
    North,
    East,
    South,
    West,
}

fn turn(d: Direction) -> Direction {
    match d {
        Direction::North => Direction::East,
        Direction::East => Direction::South,
        Direction::South => Direction::West,
    }
}

fn main() {
    let _ = turn(Direction::North);
}

The turn function forgets the West variant.

match d {
    Direction::North => Direction::East,
    Direction::East => Direction::South,
    Direction::South => Direction::West,
}

Error E0004 names the missing pattern. This check is why it is better to list enum variants explicitly than to end the match with _: when a variant is added later, the compiler points to every match that needs updating.

$ cargo run -q
error[E0004]: non-exhaustive patterns: `Direction::West` not covered
  --> src/main.rs:9:11
   |
 9 |     match d {
   |           ^ pattern `Direction::West` not covered
   |
note: `Direction` defined here
  --> src/main.rs:1:6
   |
 1 | enum Direction {
   |      ^^^^^^^^^
...
 5 |     West,
   |     ---- not covered
   = note: the matched value is of type `Direction`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
12 ~         Direction::South => Direction::West,
13 ~         Direction::West => todo!(),
   |

For more information about this error, try `rustc --explain E0004`.

Unreachable patterns

The compiler also detects arms that can never be reached.

main.rs
fn main() {
    let n: u8 = std::env::args().count() as u8;
    let size = match n {
        _ => "any",
        0..=9 => "small",
    };
    println!("{size}");
}

The wildcard in the first arm matches everything, so the range arm below it never runs.

_ => "any",
0..=9 => "small",

This is a warning, not an error, and the program runs. It usually means that the arms are in the wrong order: more specific patterns must come before more general ones.

$ cargo run -q
warning: unreachable pattern
 --> src/main.rs:5:9
  |
4 |         _ => "any",
  |         - matches any value
5 |         0..=9 => "small",
  |         ^^^^^ no value can reach this
  |
  = note: `#[warn(unreachable_patterns)]` (part of `#[warn(unused)]`) on by default

any

Source

The Rust Reference: Patterns

The Rust Book: Patterns and Matching

std::matches documentation

In this article we have worked with patterns in Rust. The basics of the match expression are covered in Rust match expression, and tuple patterns in Rust tuples.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.