Throwing & Re-throwing
throw is how a piece of code signals "I cannot continue, and I'm handing the problem to whoever called me." Most of the time that means a plain throw new SomeException(...) statement, but PHP also lets throw appear as an expression rather than a statement, and it gives you a deliberate mechanism — exception chaining — for re-throwing a failure while attaching more context without losing the original cause.
throw as a statement
The ordinary form
<?php
function requireEven(int $n): int
{
if ($n % 2 !== 0) {
throw new \InvalidArgumentException("{$n} is not even");
}
return $n;
}throw as an expression (PHP 8)
Before PHP 8, throw could only be used as a full statement, which meant it couldn't appear inside an arrow function body, a ternary, or a null-coalescing expression — those all expect a single expression, not a statement. PHP 8 made throw itself an expression, so it can now be used anywhere an expression is valid.
throw inside a ternary and an arrow function
<?php
function requireEven(int $n): int
{
return $n % 2 === 0
? $n
: throw new \InvalidArgumentException("{$n} is not even");
}
$double = fn (?int $n) => $n !== null
? $n * 2
: throw new \InvalidArgumentException('n cannot be null');
echo requireEven(4), "\n";
try {
echo $double(null);
} catch (\InvalidArgumentException $e) {
echo 'Caught: ', $e->getMessage(), "\n";
}4 Caught: n cannot be null
This is more than syntactic sugar — it means validation-and-fail logic can live in a single expression instead of forcing a function body to expand from a one-line arrow function into a full multi-line block just to fit an if/throw pair.
Re-throwing to add context
Sometimes the right response to a caught exception isn't to handle it, but to add information relevant to the current layer and let it keep propagating. Simply calling throw $e; inside a catch block re-throws the same exception instance unchanged, which is useful when a layer wants to run cleanup or logging before passing the failure upward.
Logging, then letting the same exception continue
<?php
function saveOrder(array $order): void
{
try {
writeToDatabase($order);
} catch (\PDOException $e) {
error_log('Order save failed: ' . $e->getMessage());
throw $e; // Re-throw unchanged after logging.
}
}Exception chaining with the previous argument
More often, re-throwing means wrapping the low-level failure in a more meaningful, higher-level exception type — a \\PDOException about a broken connection isn't meaningful to code three layers up that only cares about "the order couldn't be saved." Every built-in exception constructor accepts an optional third argument, $previous, specifically for this: it links the new exception to the one that caused it, without discarding the original.
Wrapping a low-level exception in a domain-specific one
<?php
class OrderSaveFailedException extends \RuntimeException
{
}
function saveOrder(array $order): void
{
try {
writeToDatabase($order);
} catch (\PDOException $e) {
throw new OrderSaveFailedException(
"Could not save order #{$order['id']}",
previous: $e,
);
}
}
try {
saveOrder(['id' => 42]);
} catch (OrderSaveFailedException $e) {
echo 'Top-level message: ', $e->getMessage(), "\n";
echo 'Caused by: ', $e->getPrevious()->getMessage(), "\n";
}Top-level message: Could not save order #42 Caused by: Connection refused
getPrevious() returns the chained exception (or null if there wasn't one), and PHP's default uncaught-exception rendering walks the entire chain automatically, printing "Next exception" sections for each link. That means even without custom logging, a stack trace for OrderSaveFailedException still shows the original \\PDOException underneath it — nothing about the root cause is lost.
Nested try/catch and re-throwing a different type
Translating an exception type at a layer boundary
<?php
function fetchUserProfile(int $userId): array
{
try {
return queryDatabase("SELECT * FROM users WHERE id = {$userId}");
} catch (\PDOException $e) {
// Callers of fetchUserProfile shouldn't need to know or catch
// PDOException -- that's a database-layer detail leaking out.
throw new \RuntimeException('Could not load user profile', previous: $e);
}
}throwworks as an expression since PHP 8, so it can appear in ternaries, arrow functions, and null-coalescing expressions.throw $e;inside acatchblock re-throws the same instance unchanged, useful for log-then-propagate.Wrapping a low-level exception in a higher-level one hides implementation details from callers further up the stack.
Pass the caught exception as the
previousconstructor argument to preserve the original cause.getPrevious()walks back to the wrapped exception; PHP's default trace output renders the whole chain automatically.