How to Match on Multiple Patterns with | in Rust

Use the pipe operator | in a match arm to group multiple patterns that share the same execution logic.

When synonyms share the same fate

You're building a command parser for a CLI tool. The user types --help, -h, or help. All three should print the usage text. You write a match statement. You start typing the arm for --help. Then you realize -h does the exact same thing. Then help does the exact same thing. Copy-pasting the body three times feels wrong. It's repetitive, and if you need to change the logic later, you have to update three places. Rust gives you a cleaner way to group these patterns.

The pipe operator | lets you list multiple patterns in a single match arm. When any of the patterns match, the code runs. You write the logic once. The compiler handles the branching.

The pipe operator as pattern OR

The pipe | acts as a logical OR within a pattern. It groups distinct values or shapes that should trigger the same execution block. Think of a bouncer checking IDs. The rule says "Allow people wearing red hats OR blue hats OR green hats". The bouncer checks the hat. If it's red, blue, or green, the person gets in. The bouncer doesn't care which specific color matched, only that one of the allowed options appeared.

In Rust, A | B | C => code checks the value against A. If it fails, it checks B. If that fails, it checks C. If any succeed, the code runs. The binding variable inside the arm refers to the matched value, regardless of which pattern succeeded. This keeps your code DRY and makes the intent clear: these patterns are equivalent for this purpose.

Treat the pipe as a logical OR for shapes. If the shapes share the fate, group them.

Minimal example

/// Parses a simple command string and prints the result.
fn parse_command(cmd: &str) {
    match cmd {
        // Group synonyms for the exit action.
        // The pipe separates alternative patterns.
        "exit" | "quit" | "q" => {
            println!("Shutting down...");
        }
        // Handle the help action.
        "help" | "h" => {
            println!("Usage: tool <command>");
        }
        // Default case for unknown commands.
        _ => {
            println!("Unknown command: {}", cmd);
        }
    }
}

fn main() {
    parse_command("quit");
}

Copy-paste is a code smell. Use the pipe to consolidate synonyms.

What happens under the hood

At compile time, Rust analyzes the patterns in each arm. It checks that the types match. If you try to match 1 | "two", the compiler rejects it because you cannot OR an integer with a string. The patterns must be compatible with the type being matched.

The compiler also checks exhaustiveness. The | operator does not change the exhaustiveness rules. You still need to cover all possible values, either by listing them or using a wildcard _. The compiler treats A | B as a single composite pattern for exhaustiveness checking.

At runtime, the match evaluates the value against the patterns in order. The | creates a composite check. The compiler often optimizes this into a jump table or a binary search, especially for enums or integer ranges. Grouping patterns can actually improve performance. It reduces the number of branches the optimizer needs to track and can enable more aggressive code generation.

Trust the optimizer. Grouping patterns often generates faster code than separate arms.

Binding symmetry: the hidden constraint

The pipe operator has a strict rule about bindings. If a pattern binds a variable, all patterns in the group must bind the same variable with the same name. This is called binding symmetry.

Consider Some(x) | None. This fails. Some binds a value to x. None has no value to bind. The compiler cannot guarantee that x exists in the arm body. If the value is None, x would be uninitialized. The compiler rejects this with a "bindings in a pattern must cover the same variants" error.

Consider Some(x) | Some(y). This also fails. Both patterns bind a value, but they use different names. The compiler doesn't know whether to call the variable x or y in the body. You must use the same name: Some(x) | Some(x). This tells the compiler that the variable is x regardless of which branch matched.

This rule applies to all bindings, including struct fields and tuple elements. If you match Point { x: 0, y: y_val } | Point { x: x_val, y: 0 }, it fails. The first pattern binds y_val. The second binds x_val. The names differ. You must use the same names, even if the values come from different positions. If the structures are too different to share names, you cannot group them with |.

Binding symmetry is a hard rule. If the names don't match, the patterns don't mix.

Realistic example: grouping enum variants

Enums often have variants that share behavior. Grouping them with | keeps the match statement focused on logic rather than listing every variant.

/// Represents the state of a background job.
enum JobStatus {
    /// Job is waiting to start.
    Pending,
    /// Job is currently running.
    Processing,
    /// Job finished successfully.
    Done,
    /// Job failed with an error message.
    Failed(String),
}

/// Prints a user-friendly status message.
fn print_status(status: JobStatus) {
    match status {
        // Group active states.
        // Both need the same UI treatment.
        JobStatus::Pending | JobStatus::Processing => {
            println!("Job is active. Please wait...");
        }
        // Success state.
        JobStatus::Done => {
            println!("Job completed.");
        }
        // Error state with data binding.
        JobStatus::Failed(msg) => {
            println!("Job failed: {}", msg);
        }
    }
}

fn main() {
    print_status(JobStatus::Processing);
}

Keep arms focused. Group by behavior, not just by type.

Pitfalls and compiler errors

The most common pitfall is binding asymmetry. If you try to group patterns with different bindings, the compiler rejects the code. The error message points to the mismatched bindings. Fix the structure by using the same binding names or by splitting the patterns into separate arms.

Another pitfall involves guards. A guard applies to the entire group. A | B if condition => ... checks the condition only if A or B matches. This is efficient. However, if the condition depends on a binding, that binding must be symmetric across all patterns. Some(x) | Some(y) if x > 0 => ... fails because of the binding mismatch. Some(x) | Some(x) if x > 0 => ... works.

Ranges also work with |. 1..=5 | 10..=15 matches any integer in those ranges. The compiler optimizes range checks well. You can combine ranges with other patterns, provided the types match.

The compiler rejects asymmetric bindings immediately. Fix the structure, not the error message.

Decision: when to use | versus alternatives

Use | in a match arm when multiple patterns trigger identical logic and have the same binding structure.

Use separate match arms when patterns share logic but require different bindings, like Some(x) versus None.

Use the matches! macro when you only need a boolean check and don't need to extract values, such as matches!(c, 'a' | 'b' | 'c').

Use an if statement when the conditions are complex boolean expressions rather than structural patterns.

Use a match with a single arm and _ when you want to ignore the value entirely; | adds no value there.

Pick the tool that matches the structure. | for same-shape groups. matches! for quick checks. Separate arms when the bindings diverge.

Where to go next