The boolean pattern check
You are parsing a configuration file. You extract a value and get an Option<i32>. You do not care about the number yet. You just need to know if the key exists before you proceed to the next step. Your instinct is to write a match block or an if let. Both work. Both are correct. Both are also heavy for a simple yes-or-no question.
Rust provides a tool for this exact cut. The matches! macro checks if a value fits a pattern and returns a boolean. It does not extract data. It does not bind variables. It answers one question: "Does this value look like this pattern?"
Think of matches! as a security gate with a shape sorter. You feed a block into the slot. The gate checks if the block is a square. If it is, the light turns green. If not, red. The gate does not keep the block. It does not tell you the size of the square. It just tells you whether the shape passed the test. matches! is that gate.
How it works
matches! is a macro. It expands at compile time into a match expression. The compiler rewrites matches!(expr, pattern) into a match that returns true for the pattern and false for everything else.
Because it is a macro, you get the full power of Rust's pattern matching with zero runtime overhead compared to a manual match. The generated code is identical to what you would write by hand. You get exhaustiveness checking, guard clauses, and destructuring syntax, all wrapped in a boolean result.
fn main() {
let score: Option<i32> = Some(100);
// Check if the option contains exactly 100.
// Returns true or false. No variable binding occurs.
let is_perfect = matches!(score, Some(100));
if is_perfect {
println!("Perfect score!");
}
}
Community convention is strict here. If you need the inner value, use if let. If you only need the truth value, use matches!. Using if let just to check existence is considered noisy. Using matches! when you need the value forces you to re-match, which is wasteful. Pick the tool that matches your intent.
Keep the boolean logic tight. matches! keeps the noise out.
Real-world usage: Enums and guards
Enums are the natural home for matches!. You often need to check if a value is one of several variants without caring about the payload.
#[derive(Debug)]
enum Status {
Active,
Pending,
Inactive,
Error(String),
}
fn is_operational(status: &Status) -> bool {
// Check multiple variants using the pipe operator.
// The pattern matches either Active or Pending.
matches!(status, Status::Active | Status::Pending)
}
fn has_error(status: &Status) -> bool {
// Check for the Error variant and ignore the message.
// The .. syntax discards the inner String.
matches!(status, Status::Error(..))
}
You can also add guards. Guards let you attach a runtime condition to a pattern. This is useful when you need to check a value range or a method result.
fn is_high_score(score: Option<i32>) -> bool {
// Match Some(n) only if n is greater than 90.
// The guard runs only when the pattern matches.
matches!(score, Some(n) if n > 90)
}
Guards make matches! incredibly flexible. You can combine structural checks with arbitrary logic. The guard expression has access to any bindings made in the pattern, but those bindings are scoped to the macro. You cannot use them outside the matches! call.
Convention aside: For Option and Result, the standard library provides is_some, is_ok, and their counterparts. Use those for simple existence checks. They read better in English. Switch to matches! the moment you need to inspect the contents, check multiple variants, or use a guard.
Functional style and iterators
Functional programming patterns love matches!. When chaining iterators, you often need a predicate function. matches! fits perfectly inside filter, find, and partition.
#[derive(Debug)]
struct User {
name: String,
active: bool,
}
fn main() {
let users = vec![
User { name: "Alice".into(), active: true },
User { name: "Bob".into(), active: false },
User { name: "Charlie".into(), active: true },
];
// Filter users where active is true.
// matches! provides a clean boolean predicate.
let active_users: Vec<&User> = users
.iter()
.filter(|u| matches!(u.active, true))
.collect();
// Partition into active and inactive.
// The closure returns true for active users.
let (active, inactive) = users.iter().partition(|u| matches!(u.active, true));
println!("Active: {:?}", active);
println!("Inactive: {:?}", inactive);
}
Using matches! in closures keeps the intent clear. The closure returns a boolean. The pattern describes the condition. There is no extra syntax clutter.
Match ergonomics apply inside matches!. You do not need to manually add & to the pattern if the value is a reference. matches!(&opt, Some(42)) works just fine. The compiler handles the borrowing automatically.
Pitfalls and gotchas
matches! has a few traps for beginners. The compiler catches most of them, but the error messages can be confusing if you do not know what to expect.
You cannot extract values with matches!. If you write matches!(opt, Some(val)), the variable val disappears immediately. The macro returns a boolean, not the data. If you try to use val after the call, the compiler rejects you with E0425 (cannot find value val in this scope).
fn main() {
let opt = Some(42);
// This compiles. val is bound inside the macro.
let exists = matches!(opt, Some(val));
// This fails. val is not available here.
// Error[E0425]: cannot find value `val` in this scope
println!("Value: {}", val);
}
If you need the value, use if let. if let binds the variable and makes it available in the block.
matches! also swallows non-exhaustive warnings. If you check matches!(x, Some(_)) on an Option, the compiler will not warn you about None. That is intentional. The macro adds a catch-all arm that returns false. This design allows matches! to be used for partial checks without forcing you to handle every case.
Be careful with complex patterns. matches! shines for simple checks. If the pattern becomes deeply nested or requires multiple guards, readability drops. At that point, a full match expression is often clearer.
If you need the data, matches! is the wrong tool. Reach for if let.
Decision matrix
Use matches! when you need a boolean result from a pattern check and do not need to extract any data.
Use if let when you need to check a pattern and immediately use the extracted values inside the block.
Use match when you have multiple distinct cases to handle, each requiring different logic or value extraction.
Use matches! with guards when you need to check a pattern combined with a runtime condition, like checking a range or a method result.
Use is_some or is_ok for simple existence checks on Option and Result when readability matters more than pattern flexibility.
Use matches! inside iterator adapters like filter and partition to provide clean boolean predicates based on structure.