skip to content

In PHP 8.0 and later, how can throw be used inside an expression, and when may a catch block omit its variable?

level: middleimportance: nice to knowfreq 26%

answer

  1. statement became an expression in 8.0
  2. ?? throw and ?: throw
  3. fn() => throw
  4. throw has the lowest precedence
  5. catch (CacheMiss) with no $e

basics

~20 s

Since 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 s

Before 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
<?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

for a junior

Recall that since PHP 8.0 throw can follow ?? or ?: and that catch (SomeType) without a variable is allowed.

for a middle

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.

for a senior

Use throw expressions for short guards without hiding failure paths, and keep catch variables wherever logging, wrapping or rethrowing might be needed later.

for a principal

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