The match Expression
Reviewed & published by Brayan K
PHP 8's match is a sharper, safer cousin of switch: it returns a value, compares strictly, never falls through, and fails loudly on the unexpected. This lesson shows when to reach for it — and the elegant match(true) trick for ranges.
Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Write a match expression that returns a value
- Group multiple values into one arm
- Understand strict === matching vs switch's ==
- Rely on no fall-through and no break
- Use match(true) for ranges and conditions
- Handle (or deliberately not) the no-match case
1️⃣ The Basics
match takes a subject and a set of arms written value => result. It returns the result of the first arm whose value matches, and because it's an expression, you can assign or echo that result directly. One arm can list several comma-separated values.
<?php
// match (PHP 8) returns a VALUE based on the first matching arm.
// Syntax: match (subject) { value => result, ..., default => result };
$status = 404;
$message = match ($status) {
200 => "OK",
301, 302 => "Redirect", // one arm can list several values
404 => "Not Found",
500 => "Server Error",
default => "Unknown",
};
echo $message . "\n"; // Not Found
// match is an EXPRESSION, so it produces a value you can assign or echo.
echo match (3) { 1 => "one", 2 => "two", 3 => "three" } . "\n"; // three
?>2️⃣ Strict Matching (=== not ==)
The headline safety feature: match compares with strict ===, whereas switch uses loose ==. So a string "1" will not match an integer 1 arm — exactly the type-juggling surprise that switch lets slip through.
<?php
// match compares with === (STRICT). switch uses == (loose).
// This avoids a whole class of juggling bugs.
$value = "1"; // a STRING
$switchResult = "(switch)";
switch ($value) {
case 1: // loose: "1" == 1 is TRUE
$switchResult = "matched int 1";
break;
default:
$switchResult = "default";
}
$matchResult = match ($value) {
1 => "matched int 1", // strict: "1" === 1 is FALSE -> skipped
"1" => "matched string 1", // this one wins
default => "default",
};
echo $switchResult . "\n"; // matched int 1 (loose surprise!)
echo $matchResult . "\n"; // matched string 1 (strict, predictable)
?>3️⃣ No Fall-Through, No break
In a switch, forgetting break makes execution "fall through" into the next case — a perennial source of bugs. match arms are self-contained: exactly one runs, and there's no break to forget.
<?php
// No fall-through, no break needed. Each arm is self-contained.
// switch needs 'break' or it keeps running into the next case.
$role = "editor";
$level = match ($role) {
"admin" => 3,
"editor" => 2,
"viewer" => 1,
default => 0,
};
echo "Access level: $level\n"; // Access level: 2
// Forgetting 'break' in switch causes accidental fall-through bugs —
// match simply cannot have that problem.
?>4️⃣ match(true) for Ranges
Here's the trick that makes match shine. Use match (true) and make each arm a boolean condition; the first one that's true wins. A long if/elseif ladder collapses into a clean, readable table — perfect for grades, price bands, and thresholds.
<?php
// match(true) is a clean way to express RANGE / condition logic.
// Each arm is a boolean; the first true arm wins.
$score = 82;
$grade = match (true) {
$score >= 90 => "A",
$score >= 80 => "B",
$score >= 70 => "C",
$score >= 60 => "D",
default => "F",
};
echo "Grade: $grade\n"; // Grade: B
// This replaces a long if/elseif ladder with a tidy table.
$temp = 31;
echo match (true) {
$temp >= 30 => "hot\n",
$temp >= 15 => "mild\n",
default => "cold\n",
}; // hot
?>5️⃣ The No-Match Case
If nothing matches and there's no default, match throws an UnhandledMatchError. That's a feature: unexpected values fail loudly during testing instead of slipping by. Add a default arm when you want a catch-all, or omit it when you want every case handled explicitly.
<?php
// If NO arm matches and there's NO default, match throws
// UnhandledMatchError — a feature: unexpected values fail loudly.
function planLabel(string $plan): string {
return match ($plan) {
"free" => "Free tier",
"pro" => "Pro plan",
// no default on purpose
};
}
echo planLabel("pro") . "\n"; // Pro plan
try {
echo planLabel("enterprise"); // not handled!
} catch (\UnhandledMatchError $e) {
echo "Caught: unknown plan\n"; // Caught: unknown plan
}
?>Now you try. Fill the ___ blanks to map an HTTP method to an action.
<?php
// 🎯 YOUR TURN — fill each ___ to map an HTTP method to an action.
$method = "POST";
$action = match ($method) {
"GET" => "read",
"POST" => ___, // 👉 "create"
"PUT", "PATCH" => "update", // 👈 two values share one arm
"DELETE" => "delete",
___ => "unknown", // 👉 the default arm keyword
};
echo $action . "\n"; // create
// ✅ Expected output:
// create
?>🧩 Reorder Challenge
These scrambled lines should map a status code to a label and print it. Put them in the order that prints Status: Not Found.
Define the subject $code first, then open the match, list the arms (each ending in a comma, including default), close it with };, and finally echo the result. Because match is an expression, the closing }; ends the assignment statement. Output: Status: Not Found.
🧠 Quick Recall
Predict the output before revealing each answer.
Prints str. match uses strict ===, so the string "1" does not match the integer 1 arm — it matches the "1" arm.
Throws an UnhandledMatchError. This is deliberate — unexpected values fail loudly rather than passing silently.
Returns C. The first arm ($n >= 90) is false; the second ($n >= 70) is the first true arm, so it wins.
Common Errors (and the fix)
- "UnhandledMatchError" — a value didn't match any arm. Add a default => ... arm, or handle the missing value before the match.
- An arm you expected to match is skipped — match is strict. A string subject won't hit an integer arm; align the types or add the correct one.
- "Parse error" after your match — you forgot the trailing ;. Since match is an expression, the statement needs }; at the end.
- You tried to run several statements in an arm — arms return a single expression. Call a function from the arm, or use a switch/if when you need multiple statements.
Pro Tips
- 💡 Reach for match when mapping a value to a value. It's shorter and safer than a switch that just assigns.
- 💡 Use match(true) to flatten if/elseif ladders for ranges, grades, and thresholds.
- 💡 Omit default on purpose for enum-like values so a forgotten case throws during testing rather than shipping a silent bug.
📋 Quick Reference — match vs switch
| Aspect | match | switch |
|---|---|---|
| Returns a value? | Yes (expression) | No (statement) |
| Comparison | === (strict) | == (loose) |
| Fall-through | No | Yes (needs break) |
| No match | Throws error | Does nothing |
| Multiple values | 1, 2 => ... | stacked case labels |
| Multi-statement arms | No | Yes |
Mini-Challenge: Shipping Cost
Write it yourself, run it on onecompiler.com/php or your own machine, then check against the expected output in the comments.
<?php
// 🎯 MINI-CHALLENGE: a shipping-cost calculator with match(true).
// No code filled in — follow the steps, then run and compare.
//
// 1. $weight = 7.5 (kg).
// 2. Use match (true) to set $cost:
// weight <= 1 -> 3.00
// weight <= 5 -> 6.00
// weight <= 10 -> 9.00
// default -> 15.00
// 3. echo "Shipping: $" . number_format($cost, 2);
//
// ✅ Expected output:
// Shipping: $9.00
// your code here
?>🎉 Lesson Complete!
- ✅ match is an expression that returns the first matching arm's value
- ✅ It compares with strict === — no type-juggling surprises
- ✅ No fall-through and no break; one arm can list several values
- ✅ match(true) turns if/elseif ladders into clean range tables
- ✅ No match with no default throws UnhandledMatchError — fail loudly
Practice quiz
What does this print? echo match("1") { 1 => "int", "1" => "str", default => "def" };
- int
- def
- str
- UnhandledMatchError
Answer: str. match compares with strict ===, so the string "1" does not equal the integer 1 arm — it matches the "1" arm and prints str.
Which comparison operator does the match expression use?
- === (strict)
- == (loose)
- <=> (spaceship)
- It depends on the values
Answer: === (strict). match always uses strict === comparison, unlike switch which uses loose ==. This avoids type-juggling surprises.
What happens when no arm matches and there is no default arm?
- Returns null
- Returns false
- Silently does nothing
- Throws UnhandledMatchError
Answer: Throws UnhandledMatchError. An unmatched match with no default throws an UnhandledMatchError — a feature, so unexpected values fail loudly.
With $n = 75, what does match(true) { $n >= 90 => "A", $n >= 70 => "C", default => "F" } return?
- A
- C
- F
- It throws an error
Answer: C. In match(true) the first arm that evaluates to true wins. $n >= 90 is false, $n >= 70 is the first true arm, so it returns C.
Is match a statement or an expression?
- An expression that returns a value
- A statement, like switch
- Neither — it is a loop
- A function call
Answer: An expression that returns a value. match is an expression: it evaluates to a single value you can assign or echo directly.
Do match arms fall through like switch cases, requiring break?
- Yes, you need break in each arm
- Only the default arm falls through
- No — exactly one arm runs and there is no break
- Only if you omit default
Answer: No — exactly one arm runs and there is no break. match has no fall-through. Each arm is self-contained, exactly one runs, and there is no break to forget.
How do you make one match arm handle several subject values?
- Stack them like case labels
- List them comma-separated: 301, 302 => "Redirect"
- Use the || operator
- You cannot — one value per arm
Answer: List them comma-separated: 301, 302 => "Redirect". An arm can list several comma-separated values, e.g. 301, 302 => "Redirect".
Why does switch matching "1" against case 1 succeed where match would not?
- switch sorts its cases first
- switch caches results
- switch only compares strings
- switch uses loose == comparison
Answer: switch uses loose == comparison. switch uses loose ==, so "1" == 1 is true. match uses strict ===, so the string would not hit an integer arm.
Can a single match arm run multiple statements?
- Yes, separate them with semicolons
- No — an arm returns a single expression
- Yes, wrap them in braces
- Only inside the default arm
Answer: No — an arm returns a single expression. match arms return a single expression. For multiple statements, call a function from the arm or use switch/if.
What is the idiomatic use of match(true)?
- To always return the default
- To loop over an array
- To replace if/elseif ladders for ranges and conditions
- To compare two booleans
Answer: To replace if/elseif ladders for ranges and conditions. match(true) makes each arm a boolean condition; the first true arm wins — perfect for grades, price bands, and thresholds.
Continue this course
- Previous: Arrays & Collections
- Next: Closures & Arrow Functions — Anonymous functions, use(), and concise fn() arrow syntax with capture
- Quick reference: PHP cheat sheet
Frequently asked questions
What is the match expression in PHP?
match is an expression introduced in PHP 8 that returns a value based on the first arm whose condition matches the subject. You write match (subject) { value => result, ... default => result }. Unlike switch, which is a statement that executes code, match evaluates to a single value you can assign or echo. It compares strictly with ===, has no fall-through (so no break statements), allows several comma-separated values per arm, and throws if nothing matches and there's no default.
How is match different from switch?
Four big differences. First, match uses strict === comparison while switch uses loose ==, so match avoids type-juggling surprises. Second, match is an expression that returns a value, whereas switch is a statement. Third, match has no fall-through and needs no break — each arm is self-contained. Fourth, an unmatched value with no default throws an UnhandledMatchError in match, but switch silently does nothing. In modern code, prefer match for value-mapping and reserve switch for cases where you genuinely want to run several statements.
What does match(true) do?
match(true) is a popular idiom for range or condition logic. Because the subject is the boolean true, each arm is itself a boolean expression, and the first arm that evaluates to true wins. This turns a long if/elseif ladder into a compact, readable table — ideal for grading scores, banding prices, or classifying temperatures. For example match (true) { $score >= 90 => 'A', $score >= 80 => 'B', default => 'F' }.
What happens if no arm matches in a match expression?
If no arm matches the subject and you haven't provided a default arm, match throws an UnhandledMatchError. This is intentional and useful: an unexpected value fails loudly and immediately rather than slipping through unnoticed, which is exactly what you want for things like enum-like values where every case should be handled. If you want a catch-all instead, add a default => ... arm; if you want strictness, omit default so new, unhandled values surface during testing.