Level Up your Rust pattern matching
Advanced pattern matching techniques and best practices in 10 minutes
Rust’s pattern matching feels simple enough: match on enums, destructure tuples, handle Option and Result. I stuck with these basics for months because, well, they work. But there’s a whole world of pattern techniques I wasn’t using. Once I discovered what’s actually possible, I kicked myself for all the verbose code I’d written.
Here’s what advanced pattern matching can look like (don’t worry if you don’t understand it now):

It took me way too long to discover these techniques. This post is so you don’t have to. I’ll cover:
The basics (plus hidden details)
Advanced techniques that matter
Best practices I learned the hard way
Let’s start with the fundamentals.
Basic pattern matching
If you’re already comfortable with basic pattern matching, feel free to skim this section.
Matching literals, variables, and wildcards
The simplest patterns match exact values or capture anything:

This example shows:
Literal matching (200)
Or patterns for multiple values (301 | 302 | 307 | 308)
Range matching (500..=599)
Variable binding (code)
Wildcard (_) to ignore the value
Destructuring tuples and structs
Pattern matching shines when breaking apart compound data:

Key points:
.. ignores remaining fields you don’t care about
Patterns are checked top-to-bottom, so active: false catches inactive users first
Working with arrays and slices
You can match on array and slice structure:

Notice the difference: The array match requires exactly 5 elements, while slice patterns adapt to any length. The .. matches however many elements are between the specified positions.
Advanced techniques
Time for the fun stuff. These techniques will make your code beautiful and a joy to write.
Taking references instead of consuming values
By default, pattern matching moves values, like this:

When we match Some(s), Rust transfers ownership of the String to variable s. This leaves data partially moved since the Option wrapper exists but its contents are gone. Rust prevents using partially moved values.
But you can work with references using two approaches below.
First approach: Match on a reference

Notice the match is on &data, Rust automatically borrows the inner fields for you.
Second approach: Use ref
You can achieve the same goal using ref:

But wait, if using & and ref is just doing the same thing, why do we even need ref?
Ref shines when you want to mix move and borrow of the same object. Look at this example:

In this example, we move user_id (cheap to copy) but borrow image (potentially large) for the quota check, and keep task intact for passing to do_task later.
Why ref works better than alternatives:
&task approach: Forces user_id to be &u64 instead of u64, adding unnecessary indirection for a Copy type
Owned matching: Consumes the entire task, making it unavailable for do_task(task) later
Match guards
So far, patterns let us match exact values or ranges. But what about much more complicated conditions?
Guards let you add any boolean expression after the pattern matches:

Important: Rust evaluates patterns first, then guards. The pattern extracts values (like url or path from Some), making them available for the guard condition. If the guard fails, Rust moves to the next pattern.
Binding the whole value with @
So far, patterns let you destructure data into its components. But what if you want to match on structure while keeping the whole value for function calls? Enter the @ symbol. Not sure why they picked that particular character, but here’s how it works:

Now that you’ve seen the advanced techniques, let’s step back and look at the bigger picture. Pattern matching isn’t limited to match expressions—Rust lets you use patterns in several different contexts, each with its own rules.
Where patterns are allowed
Rust divides pattern contexts into two categories based on whether the pattern must always succeed.
Irrefutable patterns: Destructure data with known structure (let bindings, function parameters)
Refutable patterns: Handle multiple possibilities with conditional logic (match, if let)
Irrefutable patterns
These contexts require patterns that always succeed:

Refutable patterns
These contexts handle patterns that might not match:

let-else is interesting. It’s relatively new (Rust 1.65). It requires the else block to diverge (it must return, panic!, or exit the function). This ensures the variable is definitely assigned if execution continues.
Best practices
Here are some guidelines for using pattern matching effectively in real-world Rust code.
Consider performance for hot paths
Match patterns are checked top-down, so put specific patterns before general ones to avoid unreachable arms. For complex conditions where order doesn’t affect logic, prioritize hot paths or cheaper conditions first (see example below).

Note: For simple enum or integer matches, the compiler generates efficient jump tables regardless of arm order. So this rule doesn’t apply.
Prefer match over if/else chains for enums
When handling enums, prefer match over if/else chains for better compiler optimization and exhaustiveness checking. While both approaches often compile to similar code in release mode, match gives the compiler more information to work with, and there are cases where if/else chains prevent certain optimizations that match enables.

Destructure multiple fields in one pattern
When you need several fields from a struct, destructure them all in the pattern instead of accessing them individually:

This makes the data dependencies explicit and often reads more clearly than repeated field access.
Use matches! for simple boolean checks
For yes/no questions, matches! is more concise than single-arm match:

These practices help you write pattern matching code that’s both performant and maintainable. The key is choosing the right tool for each situation rather than defaulting to complex match expressions everywhere.
Key takeaways
Pattern matching is one of Rust’s most distinctive features, and mastering it transforms how you write Rust code. If you can’t remember everything from this post, bookmark it and come back when you need any of these:
Advanced destructuring with tuples, structs, arrays, and slices
Reference patterns and match ergonomics for ownership control
Strategic use of guards, and @ bindings
Best practices for choosing the right pattern matching tool
With these techniques in your toolkit, you’ll write more expressive, safer, and more maintainable Rust code.
If you made it this far, you’re probably as obsessed with understanding how things really work as I am. I’m Cuong, and I write about Rust and programming. If you share the same passion, I’d love to connect with you. Feel free to reach out on X, LinkedIn, or subscribe to my blog (substack, medium) to keep pushing the boundaries together!