match() Exhaustiveness & UnhandledMatchError
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches' (d5). PHPStan and Psalm (cited in detection_hints.tools) at higher levels will catch missing match arms against enums, but this requires explicit configuration and isn't caught by default linters or the compiler itself.
Closest to 'simple parameterised fix' (e3). The quick_fix indicates adding a default arm or ensuring all enum cases are handled — this is typically a localized change within one file, but may require reviewing the match logic and understanding the value space.
Closest to 'localised tax' (b3). The choice of using match with or without default is confined to specific control-flow points. It doesn't impose system-wide architectural weight, but each match expression requires conscious decision about exhaustiveness strategy.
Closest to 'notable trap' (t5). The misconception states developers believe match without default is dangerous when the opposite is true — omitting default is intentional for safety. This contradicts intuition from switch statements where default feels like good practice, but it's a documented gotcha most PHP 8+ developers eventually learn.
Also Known As
TL;DR
Explanation
Unlike switch, PHP 8's match expression is strict in two ways: it uses strict comparison (===, not ==) and throws an UnhandledMatchError if no arm matches and there is no default. This transforms previously silent bugs — a switch with a missing case that fell through silently — into immediate, detectable failures. Add a default arm to handle unexpected values explicitly, or let the error surface during testing rather than silently misbehave in production. Static analysers (PHPStan, Psalm) can detect non-exhaustive match expressions against enum cases (PHP 8.1+), enforcing completeness at analysis time. Combine match with enum to get compile-time exhaustiveness guarantees.
Common Misconception
Why It Matters
Common Mistakes
- Not providing a default arm for match expressions that may receive unexpected values in production.
- Assuming match and switch are equivalent — match uses strict comparison (===), switch uses loose (==).
- Not using match as a return value — it is an expression, eliminating the need for break and temporary variables.
- Using match where the value space is open-ended (user input) without a default — UnhandledMatchError in production.
Code Examples
// switch with loose comparison and silent fallthrough:
switch ($code) {
case '200': $status = 'OK'; break; // '200' == 200 in switch
case '404': $status = 'Not Found'; // Missing break — falls through!
case '500': $status = 'Error'; break;
}
// match — strict, no fallthrough, expression:
$status = match((int)$code) {
200 => 'OK', 404 => 'Not Found', 500 => 'Error',
default => throw new InvalidArgumentException("Unknown code: $code")
};
$label = match($status) {
Status::Active => 'Active',
Status::Inactive => 'Inactive',
// UnhandledMatchError thrown for any other value — caught in tests
};