
Rust Pattern Guards: Writing Precise `match` Conditions
What is a pattern guard?
A pattern guard is an if condition attached to a match arm. It lets you accept a pattern only when an additional boolean expression is true.
match value {
pattern if condition => { /* ... */ }
_ => { /* fallback */ }
}The pattern is checked first. If it matches, Rust evaluates the guard. Only if the guard returns true does the arm execute.
This is especially useful when:
- the pattern alone is too broad
- you need to compare captured values
- you want to keep branching logic inside
match - you want to avoid nested
ifblocks after matching
A simple example
Suppose you want to classify integers by sign and parity.
fn describe(n: i32) -> &'static str {
match n {
x if x < 0 => "negative",
0 => "zero",
x if x % 2 == 0 => "positive even",
_ => "positive odd",
}
}Here, x captures the matched value, and the guard checks a property of it. This is often cleaner than matching on ranges and then adding extra logic inside each arm.
Why this is better than nested if
Without guards, you might write:
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
x => {
if x < 0 {
"negative"
} else if x % 2 == 0 {
"positive even"
} else {
"positive odd"
}
}
}
}That works, but it hides the matching intent. Pattern guards keep the decision structure visible at the match level.
How guards interact with pattern matching
A guard does not change the pattern itself. It only adds a condition after the pattern has matched.
Consider this example:
let point = (3, -2);
match point {
(x, y) if x == y => println!("on the diagonal"),
(x, y) if x > 0 && y > 0 => println!("in the first quadrant"),
_ => println!("something else"),
}The tuple pattern (x, y) matches any two-element tuple. The guard then decides which arm is valid.
Important detail: guards are evaluated after binding
Bindings created by the pattern are available inside the guard:
match Some(42) {
Some(n) if n > 40 => println!("large enough"),
Some(n) => println!("small value: {n}"),
None => println!("no value"),
}This makes guards useful for value-based filtering while still preserving the structure of the match.
Common real-world use cases
1. Filtering enum variants by content
Pattern guards are often used with enums that carry data.
enum Event {
Message { user: String, text: String },
Disconnect { user: String },
}
fn handle(event: Event) {
match event {
Event::Message { user, text } if text.starts_with("!admin") => {
println!("admin command from {user}");
}
Event::Message { user, text } => {
println!("{user}: {text}");
}
Event::Disconnect { user } => {
println!("{user} disconnected");
}
}
}This keeps the command-specific logic close to the Message variant without duplicating the variant pattern.
2. Matching ranges with extra constraints
Rust supports range patterns, but sometimes a range is not enough.
fn classify(score: u8) -> &'static str {
match score {
0..=49 => "fail",
50..=79 if score >= 60 => "pass",
50..=79 => "borderline",
80..=100 => "excellent",
_ => "invalid",
}
}This example is artificial, but it shows how guards can refine a range match when business rules are more specific than the type system alone.
3. Matching Option or Result with conditions
You may want to handle only some successful values.
fn process(result: Result<String, String>) {
match result {
Ok(text) if !text.is_empty() => println!("got text: {text}"),
Ok(_) => println!("empty success"),
Err(err) => eprintln!("error: {err}"),
}
}This avoids extracting the Ok value and then checking it in a separate if.
Guard syntax and readability
A guard uses the if keyword directly after the pattern:
pattern if condition => expressionYou can also use blocks:
match value {
x if x > 10 => {
println!("large");
}
_ => {
println!("small or equal");
}
}Best practices for readability
Pattern guards are powerful, but they can become hard to scan if overused. Keep these guidelines in mind:
- Prefer simple boolean expressions in guards.
- Avoid heavy computation inside guards.
- If the condition is complex, extract it into a helper function.
- Use guards to refine a match, not to replace all logic.
A good rule of thumb: if the guard reads like a short predicate, it probably belongs there. If it reads like a mini-program, move it out.
Guards versus if let
if let is useful when you only care about one pattern. Pattern guards are better when you need multiple branches and extra conditions.
| Construct | Best for | Example use |
|---|---|---|
if let | One pattern, optional fallback | Handle Some(x) only |
match with guards | Multiple patterns with conditions | Classify values with several branches |
Plain match | Structural branching only | Different enum variants |
Example comparison
Using if let:
if let Some(n) = maybe_number {
if n > 10 {
println!("large number");
}
}Using match with a guard:
match maybe_number {
Some(n) if n > 10 => println!("large number"),
_ => {}
}The match version is often clearer when there are several outcomes, not just one.
Guards with multiple patterns
You can combine multiple patterns with | and apply one guard to the whole arm.
match ch {
'a' | 'e' | 'i' | 'o' | 'u' if is_uppercase_context() => {
println!("uppercase vowel in context");
}
'a' | 'e' | 'i' | 'o' | 'u' => {
println!("vowel");
}
_ => {
println!("other");
}
}The guard applies after any of the listed patterns match. This is useful when several patterns share the same additional condition.
Be careful with precedence
The guard belongs to the entire arm, not just the last pattern. If you need different conditions for different patterns, split them into separate arms.
Borrowing, ownership, and guards
Pattern guards can interact with borrowed values, but they do not move values by themselves. That said, the pattern used in the arm may move or borrow depending on how you write it.
let items = vec![String::from("alpha"), String::from("beta")];
match items.first() {
Some(s) if s.len() > 3 => println!("long enough: {s}"),
Some(s) => println!("short: {s}"),
None => println!("empty"),
}Here, first() returns Option<&String>, so the guard works with a reference. This is common in Rust code because it avoids unnecessary cloning.
Practical advice
- Prefer matching on references when you only need to inspect data.
- Use guards to inspect borrowed fields without taking ownership.
- If a guard needs expensive derived data repeatedly, compute it before the
match.
When not to use a pattern guard
Pattern guards are not always the best choice.
Avoid them when:
- the condition is unrelated to the pattern
- the logic is too complex to fit in a short predicate
- the same guard is repeated across many arms
- the code becomes harder to test or reason about
For example, this is probably too much for a guard:
match request {
Request::Submit(data) if validate(&data) && rate_limit_ok() && user_is_allowed() => {
// ...
}
_ => {}
}This may be better as:
match request {
Request::Submit(data) => {
if validate(&data) && rate_limit_ok() && user_is_allowed() {
// ...
}
}
_ => {}
}The second version separates structural matching from policy checks, which can improve maintainability.
A practical design pattern: classify before acting
A common Rust style is to use match with guards to classify data, then execute a focused action in each branch.
enum HttpStatus {
Ok(u16),
Redirect(u16),
ClientError(u16),
ServerError(u16),
}
fn log_status(status: HttpStatus) {
match status {
HttpStatus::Ok(code) if code == 200 => println!("standard success"),
HttpStatus::Ok(code) => println!("other success: {code}"),
HttpStatus::Redirect(code) => println!("redirect: {code}"),
HttpStatus::ClientError(code) if code == 404 => println!("not found"),
HttpStatus::ClientError(code) => println!("client error: {code}"),
HttpStatus::ServerError(code) => println!("server error: {code}"),
}
}This style is especially useful in logging, validation, parsing, and protocol handling, where a variant alone is not enough to determine the response.
Testing guard-heavy code
Because guards encode business rules, they deserve tests. A good test suite should cover:
- each pattern branch
- guard-true and guard-false cases
- boundary values for numeric conditions
- empty, missing, and malformed inputs
For example, if a guard checks n > 10, test 10, 11, and a value that matches the pattern but fails the guard. This ensures the fallback arm behaves correctly.
Summary
Pattern guards let you write precise, expressive match statements by combining structural matching with boolean conditions. They are ideal when you want to keep branching logic readable while avoiding nested if blocks.
Use them to:
- refine enum and tuple matches
- filter values by runtime conditions
- keep classification logic centralized
- improve clarity in parsing, validation, and event handling
Use them sparingly when the condition is simple, and extract helper functions when the guard becomes too complex.
