PHPmatch Expression (PHP 8)

match Expression (PHP 8)

PHP 8 introduced match, a compact alternative to switch for picking one value out of several possibilities. At first glance it looks like a tidier switch, but it is built on different rules underneath: match is an expression that produces a value, every comparison is strict (===), and there is no accidental fall-through between arms. Those three differences make match the better default choice for most "pick a value based on another value" situations you would previously have reached for switch to solve.

Basic syntax

A match expression takes a subject value, compares it against a list of arms, and evaluates to whichever arm's result matches. Each arm is a comma-separated list of conditions, an arrow =>, and the result for that arm.

A simple match expression

PHP
<?php
$statusCode = 404;

$message = match ($statusCode) {
    200 => 'OK',
    301 => 'Moved Permanently',
    404 => 'Not Found',
    500 => 'Internal Server Error',
};

echo $message;
Not Found
match is an expression, switch is a statement

The biggest practical difference from switch is that match directly produces a value you can assign to a variable, return from a function, or pass as an argument. With switch, you have to declare a variable first and assign to it inside every case, or use return inside each branch. match removes that boilerplate entirely.

Returning directly from match

PHP
<?php
function describeHttpRange(int $code): string
{
    return match (true) {
        $code >= 200 && $code < 300 => 'Success',
        $code >= 300 && $code < 400 => 'Redirection',
        $code >= 400 && $code < 500 => 'Client Error',
        $code >= 500 => 'Server Error',
        default => 'Unknown',
    };
}

echo describeHttpRange(201);
echo "\n";
echo describeHttpRange(403);
Success
Client Error

Notice the match (true) pattern above: because match arms can be any expression, not just literal values, using true as the subject lets each arm act as its own boolean condition. This is a common idiom for range checks and other logic that does not map cleanly onto a single fixed value.

Strict comparison: match uses ===, switch uses ==

This is the difference that catches the most people off guard. switch compares each case using loose equality (==), which triggers PHP's type juggling rules. match always compares using strict equality (===), so both the value and the type must agree. The classic example is comparing the string "0" against the integer 0.

switch (loose ==) vs match (strict ===)

PHP
<?php
$input = '0';

switch ($input) {
    case 0:
        echo "switch: matched 0 (loose ==)\n";
        break;
    default:
        echo "switch: no match\n";
}

echo match ($input) {
    0 => "match: matched 0 (strict ===)\n",
    default => "match: no match, '0' is a string, not an int\n",
};
switch: matched 0 (loose ==)
match: no match, '0' is a string, not an int

In the switch block, PHP converts '0' for the comparison and treats it as equal to the integer 0, so the first case fires. In the match block, '0' === 0 is false because one operand is a string and the other is an integer, so control falls through to default. If your data comes from user input, query strings, or JSON decoding — all of which tend to produce strings — this difference can silently change which branch runs.

Multi-condition arms

Just like switch allows several case labels to share one block by stacking them without a break, match lets you list several conditions in a single arm separated by commas. The arm runs if the subject strictly equals any of the listed values.

Grouping several values into one arm

PHP
<?php
function dayType(string $day): string
{
    return match ($day) {
        'Mon', 'Tue', 'Wed', 'Thu', 'Fri' => 'Weekday',
        'Sat', 'Sun' => 'Weekend',
        default => 'Not a valid day',
    };
}

echo dayType('Wed');
echo "\n";
echo dayType('Sun');
Weekday
Weekend
No fall-through, no break needed

switch executes every case after the first match until it hits a break, which is why forgetting a break is such a common source of bugs. match has no fall-through concept at all — exactly one arm's result is evaluated and returned, and execution never continues into the next arm. There is nothing equivalent to break to remember, and nothing equivalent to forget.

UnhandledMatchError when nothing matches

If none of the arms match and there is no default arm, match does not silently do nothing the way an unmatched switch would — it throws an UnhandledMatchError. This is a deliberate design choice: match is meant to represent an exhaustive decision, so an unexpected input becomes a loud, catchable error instead of a quiet no-op that might hide a bug.

An unmatched value throws UnhandledMatchError

PHP
<?php
function sizeLabel(string $size): string
{
    return match ($size) {
        'S' => 'Small',
        'M' => 'Medium',
        'L' => 'Large',
    };
}

echo sizeLabel('XL');
PHP Fatal error:  Uncaught UnhandledMatchError: Unhandled match case 'XL'

You have two ways to deal with this: add a default arm to cover every other case, or catch UnhandledMatchError explicitly when you want to react to an invalid value rather than silently substitute one.

Option 1: add a default arm

PHP
<?php
function sizeLabel(string $size): string
{
    return match ($size) {
        'S' => 'Small',
        'M' => 'Medium',
        'L' => 'Large',
        default => 'Unknown size',
    };
}

echo sizeLabel('XL');
Unknown size

Option 2: catch UnhandledMatchError explicitly

PHP
<?php
function sizeLabel(string $size): string
{
    return match ($size) {
        'S' => 'Small',
        'M' => 'Medium',
        'L' => 'Large',
    };
}

try {
    echo sizeLabel('XL');
} catch (\UnhandledMatchError $e) {
    echo "Invalid size supplied: " . $e->getMessage();
}
Invalid size supplied: Unhandled match case 'XL'
A missing default is not always a bug, but it can surprise you
It is tempting to leave off `default` because "every value is covered right now." The problem is that `match` throws at **runtime**, not compile time — if the subject can ever come from outside your control (user input, an API response, a future enum case someone adds), a value you didn't anticipate will crash the request with `UnhandledMatchError` instead of failing gracefully. If the set of valid inputs is not airtight, add a `default` arm or wrap the `match` in a `try`/`catch`.
match with no subject value

Because match arms can hold arbitrary boolean expressions, you can write match (true) to replace a chain of if/elseif statements that each check unrelated conditions, similar to the HTTP range example earlier. This keeps the code declarative and guarantees only one branch runs, the same guarantee match gives you with simple value comparisons.

Replacing if/elseif with match (true)

PHP
<?php
$score = 82;

$grade = match (true) {
    $score >= 90 => 'A',
    $score >= 80 => 'B',
    $score >= 70 => 'C',
    $score >= 60 => 'D',
    default => 'F',
};

echo "Grade: {$grade}";
Grade: B
match vs switch vs if/elseif
  • switch is a statement: it does not produce a value, allows fall-through between cases, and compares with loose ==.

  • if/elseif is the most flexible option and the right tool when conditions are unrelated or too complex for either switch or match.

  • match is an expression: it always produces a value, never falls through, compares with strict ===, and throws UnhandledMatchError instead of silently matching nothing.

Tip
Reach for `match` by default whenever you are mapping one input to one output value — it is shorter, safer (strict comparison, no accidental fall-through), and reads closer to a lookup table than a control-flow statement. Keep `switch` or `if`/`elseif` for cases where you genuinely need loose comparison, multiple statements per branch, or intentional fall-through behaviour.