skip to content
Mehdi Mehdikhani
Table of Contents

Pattern matching in Rust is way more powerful than switch statements in other languages. It’s not just about comparing values - you can destructure complex data and extract exactly what you need.

What Pattern Matching Actually Does

match expressions let you compare a value against patterns and execute code based on which pattern matches:

enum Status {
Loading,
Success(String),
Error(String),
}
fn handle_status(status: Status) {
match status {
Status::Loading => println!("Still loading..."),
Status::Success(data) => println!("Got data: {}", data),
Status::Error(msg) => eprintln!("Error: {}", msg),
}
}

The compiler ensures you handle every possible case, so there’s no forgotten error handling.

Destructuring Everything

You can destructure structs, tuples, arrays, and more:

struct Point {
x: i32,
y: i32,
}
fn describe_point(point: Point) {
match point {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x: 0, y } => println!("On Y-axis at {}", y),
Point { x, y: 0 } => println!("On X-axis at {}", x),
Point { x, y } if x == y => println!("On diagonal at {}", x),
Point { x, y } => println!("Point at ({}, {})", x, y),
}
}

The if guard lets you add extra conditions to patterns.

When I Actually Use Pattern Matching

Most of the time, I reach for pattern matching when:

  1. Handling Results and Options: The bread and butter of Rust error handling:
fn read_config() -> Result<String, std::io::Error> {
std::fs::read_to_string("config.toml")
}
fn load_application() {
match read_config() {
Ok(content) => {
println!("Config loaded: {} bytes", content.len());
// Parse and use config...
},
Err(e) => {
eprintln!("Failed to read config: {}", e);
println!("Using default configuration");
}
}
}
  1. Processing JSON or API responses: Destructuring nested data:
use serde_json::Value;
fn process_api_response(json: Value) {
match json {
Value::Object(map) => {
match (map.get("status"), map.get("data")) {
(Some(Value::String(status)), Some(data)) if status == "success" => {
println!("Success! Data: {}", data);
},
(Some(Value::String(status)), Some(Value::String(error))) if status == "error" => {
eprintln!("API Error: {}", error);
},
_ => eprintln!("Unexpected response format"),
}
},
_ => eprintln!("Expected JSON object"),
}
}
  1. State machines: Perfect for handling different states:
enum ConnectionState {
Disconnected,
Connecting { timeout: u64 },
Connected { session_id: String },
Reconnecting { attempts: u32, last_error: String },
}
fn handle_connection_event(state: ConnectionState, event: &str) -> ConnectionState {
match (state, event) {
(ConnectionState::Disconnected, "connect") => {
println!("Starting connection...");
ConnectionState::Connecting { timeout: 30 }
},
(ConnectionState::Connecting { .. }, "success") => {
let session_id = generate_session_id();
println!("Connected with session: {}", session_id);
ConnectionState::Connected { session_id }
},
(ConnectionState::Connecting { .. }, "timeout") => {
println!("Connection timed out, will retry");
ConnectionState::Reconnecting {
attempts: 1,
last_error: "timeout".to_string()
}
},
(ConnectionState::Connected { session_id }, "disconnect") => {
println!("Disconnecting session: {}", session_id);
ConnectionState::Disconnected
},
(ConnectionState::Reconnecting { attempts, .. }, "retry") if attempts < 3 => {
println!("Retry attempt {}", attempts + 1);
ConnectionState::Connecting { timeout: 30 }
},
(state, event) => {
println!("Ignoring event '{}' in state {:?}", event, state);
state
}
}
}
  1. Parsing and tokenizing: Breaking down input data:
#[derive(Debug)]
enum Token {
Number(f64),
Operator(char),
Identifier(String),
LeftParen,
RightParen,
}
fn parse_tokens(tokens: &[Token]) -> Result<f64, String> {
match tokens {
// Simple number
[Token::Number(n)] => Ok(*n),
// Binary operation: number operator number
[Token::Number(a), Token::Operator(op), Token::Number(b)] => {
match op {
'+' => Ok(a + b),
'-' => Ok(a - b),
'*' => Ok(a * b),
'/' if *b != 0.0 => Ok(a / b),
'/' => Err("Division by zero".to_string()),
_ => Err(format!("Unknown operator: {}", op)),
}
},
// Parentheses: ( expression )
[Token::LeftParen, middle @ .., Token::RightParen] => {
parse_tokens(middle)
},
_ => Err("Invalid expression".to_string()),
}
}

Advanced Pattern Features

Rust patterns have some really nice features:

Range Patterns

fn categorize_score(score: u32) -> &'static str {
match score {
0..=59 => "F",
60..=69 => "D",
70..=79 => "C",
80..=89 => "B",
90..=100 => "A",
_ => "Invalid score",
}
}

Multiple Patterns

fn is_weekend(day: &str) -> bool {
match day {
"Saturday" | "Sunday" => true,
_ => false,
}
}

Binding with @

enum Message {
Text(String),
Image { url: String, width: u32, height: u32 },
}
fn process_message(msg: Message) {
match msg {
Message::Text(content) if content.len() > 100 => {
println!("Long message: {}...", &content[..100]);
},
Message::Text(content) => {
println!("Message: {}", content);
},
Message::Image { width, height, .. } if width * height > 1_000_000 => {
println!("Large image: {}x{}", width, height);
},
Message::Image { url, .. } => {
println!("Image: {}", url);
},
}
}

Real World Example

Here’s how I use pattern matching in a log parser:

#[derive(Debug)]
enum LogLevel {
Info,
Warning,
Error,
}
#[derive(Debug)]
struct LogEntry {
timestamp: String,
level: LogLevel,
module: String,
message: String,
}
fn parse_log_line(line: &str) -> Option<LogEntry> {
let parts: Vec<&str> = line.splitn(4, ' ').collect();
match parts.as_slice() {
[timestamp, level_str, module, message] => {
let level = match *level_str {
"INFO" => LogLevel::Info,
"WARN" | "WARNING" => LogLevel::Warning,
"ERROR" | "ERR" => LogLevel::Error,
_ => return None,
};
Some(LogEntry {
timestamp: timestamp.to_string(),
level,
module: module.to_string(),
message: message.to_string(),
})
},
_ => None,
}
}
fn analyze_logs(lines: &[String]) {
let mut error_count = 0;
let mut warning_count = 0;
for line in lines {
match parse_log_line(line) {
Some(LogEntry { level: LogLevel::Error, module, message }) => {
error_count += 1;
if module == "database" {
println!("DB Error: {}", message);
}
},
Some(LogEntry { level: LogLevel::Warning, .. }) => {
warning_count += 1;
},
Some(LogEntry { level: LogLevel::Info, .. }) => {
// Just count info messages
},
None => {
eprintln!("Failed to parse line: {}", line);
}
}
}
println!("Summary: {} errors, {} warnings", error_count, warning_count);
}

if let for Simple Cases

When you only care about one pattern, if let is cleaner than match:

// Instead of this:
match some_option {
Some(value) => println!("Got: {}", value),
None => {},
}
// Use this:
if let Some(value) = some_option {
println!("Got: {}", value);
}
// Works great for error handling too:
if let Err(e) = risky_operation() {
eprintln!("Operation failed: {}", e);
return;
}

while let for Iterating

while let is perfect for processing until you hit a certain pattern:

let mut stack = vec![1, 2, 3, 4, 5];
while let Some(value) = stack.pop() {
println!("Processing: {}", value);
if value == 3 {
println!("Found target, stopping");
break;
}
}

The Pattern I Follow

I use match when:

  • I need to handle multiple cases
  • I want exhaustive checking
  • I’m working with enums or complex data

I use if let when:

  • I only care about one specific pattern
  • The other cases don’t need handling

I use while let when:

  • I’m consuming an iterator until a condition
  • Processing a stack or queue

Pattern matching makes Rust code expressive. Once you get comfortable with it, you start thinking in terms of patterns, and it changes how you structure data and logic.