match expression vs. switch in PHP 8.4: Strict Comparison in Detail
AI generated
<?php
8.4
PHP · PHP 8.4 · Core Language · Control Flow
match expression vs. switch in PHP 8.4
strict comparison, expression value, and exhaustiveness in detail

Anyone who treats the match expression and switch as interchangeable overlooks solid differences: match compares strictly with ===, has no fallthrough, and throws an UnhandledMatchError on an unhandled case, while switch compares loosely with == and simply does nothing without a match. This article uses PHP 8.4 code examples to show when the match expression is the more robust choice and where switch still has its place.

15 min read match expression · strict comparison · UnhandledMatchError · match(true) PHP 8.0 · 8.1 · 8.2 · 8.3 · 8.4

1. Classification: match as an Expression, switch as a Statement

The fundamental difference between the two constructs is not a minor detail, it is the root of every other difference between them: the match expression, part of the language since PHP 8.0, is an expression and always returns a value. switch, on the other hand, is a classic control structure, a statement that only executes code branches but does not itself produce a value. This distinction sounds academic, but it has a direct effect on every single line of code written with a match expression instead of switch.

In practice this means: a match expression can be assigned directly to a variable, returned directly, or passed directly as a function argument, without a helper variable and without touching the outer scope. A switch needs a pre-declared variable for the same result, one that gets set in every case branch and is only read after the construct ends. This extra indirection is exactly where switch blocks commonly forget to set the variable in one branch, or where a value gets overwritten by a later branch.

The match expression is therefore not pattern matching in the sense of Rust or Haskell, but pure value equality between a subject and a list of conditions, combined with the property of yielding a value. Anyone who internalizes this distinction between expression and statement from the start also understands the remaining differences between match and switch, from the kind of comparison to error handling, as a logical consequence rather than arbitrary individual rules.

2. Strict Comparison with match vs. Loose Type Juggling with switch

The match expression compares the subject with every arm condition exclusively using the === operator, strict comparison. Type and value must be identical for an arm to match. switch, in contrast, compares with ==, loose comparison, which allows type juggling. PHP 8 reworked the comparison logic between numbers and strings so that 0 == "abc" now returns false, but 0 == "0" is still true, as is "1" == "01" and true == "some-string".

A widespread misconception concerns declare(strict_types=1): this directive changes nothing about the comparison behavior of switch. strict_types only affects type checking of function parameters and return values, not the == and === operators. A switch stays loosely comparing regardless of whether strict_types is active, while the match expression always compares strictly, independent of that directive. Anyone who tries to secure switch purely through strict_types is mistaken on exactly this point.

In practice, this means for the match expression: values must carry the exact type, otherwise no arm applies. match(0) does not match an arm with the string '0', because 0 !== '0'. That forces clean typing right when the code is written and surfaces bugs that appear to work with switch through silent type coercion, but in reality rest on chance rather than intent.


<?php

declare(strict_types=1);

// switch uses loose comparison (==), not affected by strict_types
function classifyLoose(mixed $value): string
{
    switch ($value) {
        case 0:
            return 'zero (loose match)';
        case '0':
            return 'string zero (unreachable, "0" == 0 already matched above)';
        case true:
            return 'boolean true (loose match)';
        default:
            return 'no match';
    }
}

// match expression uses strict comparison (===), type AND value must be identical
function classifyStrict(mixed $value): string
{
    return match ($value) {
        0 => 'int zero (strict match)',
        '0' => 'string zero (strict match)',
        true => 'boolean true (strict match)',
        default => 'no match',
    };
}

echo classifyLoose('0');   // "zero (loose match)", "0" == 0 is true
echo classifyStrict('0');  // "string zero (strict match)", '0' === '0'

// Unhandled case: match throws instead of silently doing nothing
function classifyStatus(string $status): string
{
    return match ($status) {
        'draft', 'pending' => 'not published',
        'published' => 'live',
        // no default arm on purpose
    };
}

try {
    echo classifyStatus('archived');
} catch (\UnhandledMatchError $e) {
    echo 'Unhandled value: ' . $e->getMessage();
}

3. No Fallthrough and No break in the match Expression

switch requires an explicit break at the end of every case branch, otherwise execution runs unchecked into the next case, so-called fallthrough. A forgotten break is one of the most widespread bugs in PHP code, and numerous linter rules exist solely to catch exactly this mistake before it reaches production.

The match expression has no fallthrough at all. Every arm is fully isolated: as soon as an arm matches, it returns its value and the entire match expression ends immediately, no matter how many further arms follow. There is no break keyword in the match expression, because it simply is not needed semantically. This property alone eliminates an entire class of bugs that remains structurally possible with switch.

For developers coming from languages with fallthrough by default, classic C or PHP's own switch, this requires a small adjustment. But once you understand that the match expression fundamentally never lets execution skip into another branch, you gain lasting confidence: a match arm always does exactly what it describes, never more and never less.


<?php

declare(strict_types=1);

// WRONG: missing break causes fallthrough into the next case
function discountLegacy(string $tier): float
{
    $discount = 0.0;
    switch ($tier) {
        case 'gold':
            $discount = 0.20;
        case 'silver':
            $discount = 0.10;
            break;
        case 'bronze':
            $discount = 0.05;
            break;
        default:
            $discount = 0.0;
    }
    return $discount;
}

// Bug: discountLegacy('gold') returns 0.10, not 0.20,
// because the missing break falls through into the 'silver' case.

// RIGHT: the match expression has no fallthrough by design
function discountMatch(string $tier): float
{
    return match ($tier) {
        'gold' => 0.20,
        'silver' => 0.10,
        'bronze' => 0.05,
        default => 0.0,
    };
}

echo discountLegacy('gold'); // 0.1 (bug)
echo discountMatch('gold');  // 0.2 (correct)

4. Multiple Conditions per Arm vs. switch-case Grouping

To map several values to the same code path, switch uses a deliberate exploitation of fallthrough: several case labels are stacked directly one after another, with no break in between, followed by the shared code block only at the last label. This technique works reliably, but it is exactly the tool described as a bug source in the previous section, this time used deliberately instead of by accident.

The match expression offers a native, much more explicit syntax for this: several comma-separated conditions inside a single arm, for example 200, 201, 204 => 'success'. Each of the comma-separated conditions is compared individually and strictly against the subject, and the first match decides which value is returned. This is not fallthrough, it is a genuine OR combination of conditions within the same arm.

This pattern of the match expression reads more linearly than the nested switch fallthrough structure and makes it immediately visible which values belong together, without having to search the code for the next break. Especially when categorizing HTTP status codes or enum values, this clarity pays off noticeably in larger codebases.


<?php

declare(strict_types=1);

// switch: grouping via intentional fallthrough between case labels
function statusGroupSwitch(int $code): string
{
    switch ($code) {
        case 200:
        case 201:
        case 204:
            return 'success';
        case 301:
        case 302:
        case 307:
            return 'redirect';
        case 400:
        case 404:
        case 422:
            return 'client error';
        default:
            return 'unknown';
    }
}

// match expression: comma-separated conditions inside a single arm
function statusGroupMatch(int $code): string
{
    return match ($code) {
        200, 201, 204 => 'success',
        301, 302, 307 => 'redirect',
        400, 404, 422 => 'client error',
        default => 'unknown',
    };
}

echo statusGroupMatch(204); // "success"

5. Exhaustiveness and UnhandledMatchError vs. switch default Fallback

A switch without a default branch and without a matching case simply does nothing: execution jumps past the entire construct, with no error, no warning, no feedback at all. That can cause a new status value, a new enum case, or a previously unknown input value to be silently ignored, often noticed only once a customer complains about missing behavior.

The match expression behaves fundamentally differently in exactly this case: if no arm matches and no default arm exists, PHP throws an UnhandledMatchError exception. This forces exhaustiveness at runtime. An unhandled case becomes immediately visible through this error instead of quietly creeping into production and producing wrong results there unnoticed.

This behavior of the match expression becomes especially valuable in combination with enums, part of the language since PHP 8.1. A match expression over all cases of a backed enum, deliberately written without a default arm, forces you to update the corresponding match expression whenever the enum gains a new case. Otherwise the UnhandledMatchError strikes immediately and unambiguously on the first call with the new value, instead of disguising itself as a hard-to-find bug.

6. match as an Expression: Direct Assignment and Return Values

Because the match expression is a genuine expression, its result can appear anywhere a value is expected: in an assignment, as a function argument, directly after a return, or even nested as part of another expression. This property is the actual core of what distinguishes match from switch, and it runs through every practical example in this article.

It eliminates the classic switch pattern of first declaring a helper variable, filling it in every case branch, and only using it after the entire construct completes. Fewer lines here do not just mean less typing, above all they mean fewer opportunities to accidentally forget the assignment in a single case branch, a mistake that only shows up with switch at runtime.

The readability of function bodies benefits significantly too: return match($status) { ... }; makes the complete mapping from input to output visible at a glance, without having to follow the function's control flow line by line. The reader sees immediately: this function maps one value to another, nothing more, and a well-placed match expression delivers exactly that better than any switch statement.

7. match(true) as a Replacement for if/elseif Chains

match(true) is an established idiom for replacing long if/elseif chains with a single match expression. The subject here is the boolean value true itself, while every arm contains its own condition as an expression that evaluates to true or false. Only an arm whose condition evaluates to exactly true can match.

Because the match expression compares strictly with ===, every arm condition must actually yield the boolean value true to apply, not merely a "truthy" value. The conditions are evaluated in order until the first one evaluates to true, all arms after that are skipped, exactly like an elseif chain with the same short-circuit behavior.

The use case for match(true) arises wherever plain value equality of an ordinary match($value) is not sufficient, for example range checks, several independent conditions, or combinations of different variables. The match expression remains an expression that can be returned or assigned directly, which is not possible with a classic if/elseif chain.


<?php

declare(strict_types=1);

// Classic if/elseif chain
function classifyAgeLegacy(int $age): string
{
    if ($age < 0) {
        return 'invalid';
    } elseif ($age < 13) {
        return 'child';
    } elseif ($age < 18) {
        return 'teenager';
    } elseif ($age < 67) {
        return 'adult';
    } else {
        return 'senior';
    }
}

// match(true): each arm is a boolean condition, first true wins
function classifyAgeMatch(int $age): string
{
    return match (true) {
        $age < 0 => 'invalid',
        $age < 13 => 'child',
        $age < 18 => 'teenager',
        $age < 67 => 'adult',
        default => 'senior',
    };
}

echo classifyAgeMatch(15); // "teenager"

8. match vs. switch Compared Directly

The previous sections examined each individual difference on its own. The following table places the most important aspects of the match expression and switch directly side by side, so the decision does not need to be re-derived in every code review but stays available for quick reference.

Aspect switch (Statement) match expression (Expression) Benefit
Comparison type == (loose, type juggling) === (strict, type and value) No implicit type bugs
Fallthrough Requires break per case No fallthrough, no break Eliminates forgotten-break bugs
Return value Statement, returns no value Expression, returns a value directly Fewer helper variables
No match Without default: nothing happens Without default: UnhandledMatchError Exhaustiveness at runtime
Multiple conditions Group case labels via fallthrough Comma-separated conditions per arm Linear, readable grouping
Dispatch on literals Sequential case checking possible Hash-based jump for literal arms Comparable performance on literals

In practice, the performance difference between the match expression and switch is small for purely literal, scalar conditions, since both constructs can be handled with similar efficiency by the Zend Engine. The real benefit of the match expression lies not in raw execution speed but in correctness: strict comparison prevents type bugs, the absence of fallthrough prevents forgotten break statements, and UnhandledMatchError surfaces unhandled cases instead of swallowing them.

9. Migration Strategy: switch to match Expression Step by Step

The first step of any migration is identifying suitable candidates: switch blocks that merely compute a value and return it or assign it to a variable, without side effects in individual case branches, can usually be converted one to one into a match expression. This category benefits the most, since both the expression property and the strict comparison take immediate effect here.

The second step is a type check of every single case: because a match expression compares strictly, the type of the subject must exactly match the arm conditions. If the existing switch code relies on implicit type coercion, for example a string loosely compared against an integer condition, an unreflected migration changes the runtime behavior. Here it helps to deliberately cast or normalize the input value before the match expression.

The third step concerns switch blocks with several statements and real side effects per case: these often reasonably remain a switch, or get split into a match expression for the pure condition logic plus a separate function for the actual action. It is also advisable to deliberately leave out a default in the match expression, so that UnhandledMatchError automatically surfaces new, previously unhandled enum values or status codes, instead of letting them disappear into a generic default branch.


<?php

declare(strict_types=1);

// BEFORE: switch with implicit type coercion and shared fallthrough logic
function renderBadgeLegacy($priority): string
{
    switch ($priority) {
        case 'high':
        case 3:
            $label = 'High';
            break;
        case 'medium':
        case 2:
            $label = 'Medium';
            break;
        default:
            $label = 'Low';
    }
    return $label;
}

// AFTER: match expression with explicit types and enforced exhaustiveness
enum Priority: int
{
    case High = 3;
    case Medium = 2;
    case Low = 1;
}

function renderBadgeMatch(Priority $priority): string
{
    return match ($priority) {
        Priority::High => 'High',
        Priority::Medium => 'Medium',
        Priority::Low => 'Low',
    };
}

// Adding a new case to the enum without touching renderBadgeMatch()
// immediately surfaces as an UnhandledMatchError at the call site,
// instead of silently falling back to a wrong label.

10. Summary

The match expression and switch solve the same problem at first glance, but differ fundamentally on closer inspection. match compares strictly with ===, has no fallthrough, returns a value directly as a genuine expression, and enforces exhaustiveness at runtime through UnhandledMatchError. switch compares loosely with ==, requires an explicit break, is a pure statement without a return value, and simply does nothing without a default branch.

For new code where a value mapping is the central concern, the match expression is generally the more robust and more readable choice. switch remains sensible when several statements with real side effects are needed per branch, or when loose comparison is genuinely intended, which is rarely the case in practice. Anyone migrating existing switch code should check type consistency and side effects per branch beforehand to avoid surprises from the strict comparison logic.

match expression vs. switch: The Essentials at a Glance

Strict Comparison

match compares with ===, switch with ==. declare(strict_types=1) changes nothing here, it only affects function signatures.

No Fallthrough

Every match arm is isolated, no break needed. switch runs into the next case without a break.

Expression Value

match returns a value directly for assignment or return. switch is a pure statement without a return value.

Exhaustiveness

match throws UnhandledMatchError without a matching arm. switch simply does nothing at all without a default.

11. FAQ: match expression vs. switch

1Main difference between match and switch?
match is an expression with a return value, switch is a statement without one. All further differences follow from that.
2Why strict vs. loose comparison?
match uses === (type and value), switch uses == (type juggling allowed). A deliberate design decision of match against type-related bugs.
3Does strict_types change switch behavior?
No. strict_types only affects function signatures, not the == and === operators. switch always stays loosely comparing.
4What happens without a matching arm?
PHP throws an UnhandledMatchError. This forces exhaustiveness at runtime, instead of silently ignoring the case like switch without a default.
5Multiple values in a single match arm?
Yes, comma-separated: 1, 2, 3 => 'low'. Each condition is checked strictly, with no fallthrough.
6What is match(true)?
Replaces if/elseif chains with a match expression that has a boolean condition per arm. Useful for range checks and combined conditions.
7match directly as a return value?
Yes, return match($status) { ... }; works directly. switch always needs a helper variable for that.
8Is match more performant than switch?
Barely a difference for literals. The advantage of match lies in correctness through strict comparison, not raw speed.
9Is break needed in match?
No, match has no fallthrough. As soon as an arm matches, the expression ends immediately with its value.
10How do I migrate switch to match?
First migrate pure value mappings without side effects, then check every case for type coercion. Blocks with real side effects often stay as switch.

Mironsoft

PHP architecture, code quality, and Magento development

Migrate existing switch code safely to the match expression?

We review existing PHP code for fragile switch blocks and replace them, where it makes sense, with clear match expressions using strict comparison, enforced exhaustiveness, and full PHPStan coverage at level 5 and above.

Code Review

Analysis of fragile switch blocks and proposals for match refactors

Refactoring

Migrating existing switch statements to the match expression

PHPStan Coverage

Static analysis at level 5 and above for new match structures