In PHP 8.0 and later, how can throw be used inside an expression, and when may a catch block omit its variable?
answer
- statement became an expression in 8.0
- ?? throw and ?: throw
- fn() => throw
- throw has the lowest precedence
- catch (CacheMiss) with no $e
basics
~20 sSince PHP 8.0 throw is an expression, so it can follow ??, ?: or || and form an arrow function's body. PHP 8.0 also made the catch variable optional: catch (CacheMiss) handles the type without binding the object.
solid answer
~40 sBefore PHP 8.0 `throw` was a statement, so a guard needed its own `if` block. Since 8.0 it is an expression, which allows `$user = $repo->find($id) ?? throw new UserNotFound($id);`, `$name = $input ?: throw new InvalidArgumentException('empty name');` and `fn() => throw new LogicException('not implemented')`. `throw` has the lowest precedence of all, so everything to its right belongs to the thrown value: `throw $a ?? $b` throws whichever is not null. The same release made the variable in `catch` optional: `catch (CacheMiss)` runs the block without binding the object, which documents that only the type matters and avoids an unused variable — but it also means that block cannot rethrow the object. Multi-catch with `|` is older; it arrived in PHP 7.1.
code
php · 19 lines<?php
declare(strict_types=1);
final class UserNotFound extends RuntimeException {}
function findUser(array $users, int $id): array
{
return $users[$id] ?? throw new UserNotFound("No user {$id}");
}
$users = [7 => ['name' => 'Ada']];
echo findUser($users, 7)['name'], PHP_EOL; // Ada
try {
findUser($users, 8);
} catch (UserNotFound) {
echo "not found", PHP_EOL; // the object is not needed here
}go deeper
Recall that since PHP 8.0 throw can follow ?? or ?: and that catch (SomeType) without a variable is allowed.
Explain which contexts a throw expression fits, why its lowest precedence makes everything to its right the thrown value, and when omitting the catch variable is wrong.
Use throw expressions for short guards without hiding failure paths, and keep catch variables wherever logging, wrapping or rethrowing might be needed later.
Decide how the team uses PHP 8.0 syntax in shared libraries that may still run on older runtimes, and set readability limits for guard one-liners.
## From statement to expression A **statement** is a complete instruction (`if`, `foreach`, `echo`); an **expression** produces a value and can appear inside other expressions. Until PHP 8.0, `throw` was a statement, so it could not appear where PHP expected a value. A "value or fail" guard needed a separate block: ```php $user = $repo->find($id); if ($user === null) { throw new UserNotFound($id); } ``` Since PHP 8.0, `throw` is an **expression**. It never produces a value — evaluating it throws — but it may now appear anywhere an expression may, which lets guards collapse into one line. ## Where a throw expression helps | Context | Example | Throws when | |---|---|---| | Null coalescing | `$repo->find($id) ?? throw new UserNotFound($id)` | the left side is `null` | | Short ternary | `$input ?: throw new InvalidArgumentException('empty name')` | the left side is falsy | | Boolean short-circuit | `$ok \|\| throw new RuntimeException('failed')` | the left side is falsy | | Arrow function body | `fn() => throw new LogicException('not implemented')` | the closure is called | | Full ternary branch | `$valid ? $value : throw new DomainException('invalid')` | the condition is falsy | The short-circuit operators matter: in `$a ?? throw ...`, the right side is evaluated only when needed, so the exception is created only on the failing path. Two everyday guards show the pattern: ```php $handle = fopen($path, 'rb') ?: throw new RuntimeException("Cannot open {$path}"); $dsn = $config['dsn'] ?? throw new LogicException('dsn is not configured'); ``` The first relies on `fopen()` returning `false` on failure (it also emits a warning), which `?:` treats as falsy. The second relies on `??` treating a missing array key like `null`, without any warning. Choosing the right operator matters: `??` reacts only to `null` and missing keys, while `?:` also fires for `0`, `''`, `'0'` and an empty array — a legitimate empty string would trigger the exception. ## Precedence `throw` has the **lowest precedence** of all operators. Everything to its right is part of the value being thrown: - `throw $primary ?? $fallback;` throws `$primary` if it is not `null`, otherwise `$fallback`; - `throw $flag ? new A() : new B();` throws one of the two objects. The other side of this: an operator whose precedence is lower than assignment changes the meaning of a whole line. In `$result = $ok and throw new RuntimeException('x');`, the assignment happens first and `and` then decides whether to throw — prefer `&&`, `||` and `??` with throw expressions, and add parentheses when in doubt. ## Catching without a variable Also since PHP 8.0, the variable in a `catch` clause is **optional**: ```php try { $value = $cache->get($key); } catch (CacheMiss) { $value = $this->loadFromDatabase($key); } ``` The block runs exactly as before; the thrown object simply is not bound to any name. This is useful when: - the **type alone** carries the information (a miss, a not-found, a timeout you will retry); - you want to make it obvious to readers and static analysers that the object is intentionally unused. It is a poor fit when you might log the exception, inspect its properties, wrap it as `$previous`, or rethrow it — PHP has no bare `throw;`, so a block without a variable has no way to send the same object onward. ## Which release brought what | Feature | Since | |---|---| | Several types in one `catch`, joined by `\|` | PHP 7.1 | | `throw` as an expression | PHP 8.0 | | `catch` without a variable | PHP 8.0 | Code targeting PHP 8.5 can use all three freely. Codebases that still support older runtimes need to avoid the 8.0 forms, since they are parse errors on 7.x. ## Readability limits Throw expressions are best for short, obvious guards. A long chain of `?:` and `??` ending in a `throw` with a multi-line message hides the failure path; an explicit `if` with an early `throw` is still clearer there.
- In PHP 8, what does throw $primary ?? $fallback; throw when $primary is null?It throws `$fallback`. `throw` has the lowest precedence, so the whole `$primary ?? $fallback` expression is evaluated first and its result is thrown. When `$primary` is not null, `$primary` is thrown instead.
- When should you keep the variable in a catch clause even on PHP 8?Whenever the block might log the exception, read its message or properties, pass it as `$previous` to a wrapper, or rethrow it. A variable-less `catch` never binds the object, and PHP has no bare `throw;`, so none of those are possible without the variable.
saying these in an interview costs you the question
- throw is still a statement, so it cannot follow ??
- catch (CacheMiss) without a variable swallows the exception without running the block
- A variable-less catch binds the exception to an implicit $e
- throw binds tightly, so throw $a ?? $b throws only $a
- Multi-catch with | also arrived in PHP 8.0