A Deep Dive into Pattern Matching in Rust
Grace Collins
Solutions Engineer · Leapcell

Introduction
In Rust, pattern matching is a powerful language feature that allows us to perform different operations based on different patterns. Pattern matching can be used in various scenarios, such as working with enum types, destructuring tuples and structs, handling conditional expressions, and more. This article provides a detailed introduction to Rust’s pattern matching syntax and demonstrates its usage and advantages through example code.
Basic Usage
Rust uses the match
keyword for pattern matching. A match
expression consists of multiple arms, each containing a pattern and a block of code to execute when the pattern matches. Rust evaluates the arms in order and executes the block corresponding to the first matching pattern. Here is a simple example:
fn main() { let number = 3; match number { 1 => println!("One"), 2 => println!("Two"), 3 => println!("Three"), _ => println!("Other"), } }
In the code above, we define a variable number
and assign it the value 3. Then we use a match
expression to match against number
. First, Rust checks the first arm with pattern 1
; since number
is not 1, that arm is skipped. It proceeds to the second arm, 2
, which also doesn't match. Finally, it checks the third arm, 3
, which matches, so it executes the block and prints Three
.
If none of the patterns match, the final underscore _
serves as a default case, similar to default
in other languages, and executes the corresponding block.
Matching Enum Types
In Rust, enums are a type that allows you to define a value that can be one of several different variants. Pattern matching is one of the most common ways to handle enums, allowing us to execute different logic depending on the variant.
Consider the following example, where we define an enum called Message
with three different variants: Move
, Write
, and ChangeColor
:
enum Message { Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), }
Now, we use a match
expression to handle the different variants of Message
:
fn process_message(msg: Message) { match msg { Message::Move { x, y } => println!("Move to coordinates (x={}, y={})", x, y), Message::Write(text) => println!("Write: {}", text), Message::ChangeColor(r, g, b) => println!("Change color to (r={}, g={}, b={})", r, g, b), } } fn main() { let msg1 = Message::Move { x: 10, y: 20 }; let msg2 = Message::Write(String::from("Hello, world!")); let msg3 = Message::ChangeColor(255, 0, 0); process_message(msg1); process_message(msg2); process_message(msg3); }
In the code above, we define a function process_message
that takes a Message
enum as its parameter. Within the match
expression, we handle different enum variants with different logic. For the Message::Move
variant, we destructure the pattern to get x
and y
, then print the coordinates. For Message::Write
, we print the string directly. For Message::ChangeColor
, we destructure r
, g
, and b
, and print the RGB values.
In the main
function, we create three different Message
values and pass them to process_message
for handling. Based on the variant, we execute different logic.
Destructuring and Matching Structs
Besides enums, Rust also supports destructuring and matching structs. A struct is a custom data type composed of multiple fields. We can use patterns to destructure structs and execute operations based on field values.
Consider the following example, where we define a struct named Point
to represent a point in a 2D space:
struct Point { x: i32, y: i32, }
Now we use a match
expression to destructure and match different Point
structs:
fn process_point(point: Point) { match point { Point { x, y } => println!("Point coordinates: x={}, y={}", x, y), } } fn main() { let p1 = Point { x: 10, y: 20 }; let p2 = Point { x: -5, y: 15 }; process_point(p1); process_point(p2); }
In the code above, we define a function process_point
that takes a Point
as a parameter. In the match
expression, we use the pattern Point { x, y }
to destructure the struct’s fields and print them.
In the main
function, we create two different Point
instances and pass them to process_point
. Pattern matching makes it easy to access and operate on struct fields.
Simplifying Matching with if let
In some cases, we only care whether a particular pattern matches and don’t need to handle other patterns. In such cases, the if let
expression can simplify the matching process.
Consider the following example where we define an enum called Value
with two variants: Number
and Text
:
enum Value { Number(i32), Text(String), }
Now we use if let
to check whether a Value
instance is a Number
:
fn main() { let value = Value::Number(42); if let Value::Number(n) = value { println!("The value is a number: {}", n); } else { println!("The value is not a number"); } }
In the code above, we define a Value
variable and assign it Value::Number(42)
. Then we use if let
to check whether it’s the Number
variant. If so, we destructure and print the number; otherwise, we print a message saying it’s not a number.
Using if let
can make code more concise and readable, especially when only one pattern is of interest.
Matching Multiple Patterns
Sometimes, we want to match multiple patterns and execute the same block of code. Rust provides the |
operator to match multiple patterns in a single arm.
Consider the following example, where we define a variable number
and match multiple patterns:
fn main() { let number = 42; match number { 0 | 1 => println!("Zero or one"), 2 | 3 | 4 => println!("Two, three, or four"), _ => println!("Other"), } }
In the code above, we use match
to evaluate number
. The first arm matches both 0 and 1 using 0 | 1
. The second arm matches 2, 3, and 4 using 2 | 3 | 4
. The final underscore _
acts as the default case for all other values.
The |
operator helps match multiple values cleanly and avoids code duplication.
if let
and while let
In addition to match
, Rust provides if let
and while let
for conditional pattern matching.
The if let
expression performs matching and executes a block if the condition is true. If it doesn't match, nothing happens.
The while let
expression works like if let
but repeats the process in a loop as long as the pattern matches.
Here is an example demonstrating both:
fn main() { let values = vec![Some(1), Some(2), None, Some(3)]; for value in values { if let Some(num) = value { println!("Number: {}", num); } else { println!("None"); } } let mut values = vec![Some(1), Some(2), None, Some(3)]; while let Some(value) = values.pop() { if let Some(num) = value { println!("Number: {}", num); } else { println!("None"); } } }
In the code above, we first define a vector of Option
values. Using a for
loop and if let
, we check whether each element is Some
and print the value, or print "None".
Next, we define another vector and use while let
to pop elements one by one. As long as an element is Some
, we print its value; otherwise, we print "None".
Using if let
and while let
, we can flexibly match conditions and handle patterns.
Exhaustiveness Checking in match
In Rust, match
expressions are exhaustive. This means the compiler checks whether all possible cases have been handled in a match
expression to ensure nothing is missed.
If a match
expression is not exhaustive, the compiler issues a warning to help prevent potential errors. To ensure exhaustiveness, we can add a _
arm at the end of the match
as a fallback case.
Here is an example that demonstrates exhaustiveness checking:
enum Color { Red, Green, Blue, } fn main() { let color = Color::Red; match color { Color::Red => println!("Red"), Color::Green => println!("Green"), // Missing the Color::Blue branch } }
In the code above, we define an enum Color
with three variants. Then we use a match
expression to match on the color
variable. We provide arms for Color::Red
and Color::Green
, but omit Color::Blue
.
When we try to compile this code, the Rust compiler produces the following warning:
warning: non-exhaustive patterns: `Color::Blue` not covered
This warning indicates that our match
expression is not exhaustive because it doesn’t handle all possible cases.
To fix this issue, we can either add a _
branch or explicitly match all enum variants.
Conclusion
Pattern matching is a powerful and flexible language feature in Rust that enables different operations to be performed based on different patterns.
This article introduced the basic usage of pattern matching in Rust, including matching enums and structs, using if let
and while let
to simplify matching logic, and ensuring exhaustiveness in match
expressions to handle all possible cases.
We are Leapcell, your top choice for hosting Rust projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ