set_exception_handler
Registers a function to run when an exception escapes every try/catch block.
set_exception_handler(?callable $callback): ?callable- Added in PHP 5.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$callback | ?callable | The function to call when an exception is not caught, declared as handler(Throwable $ex): void. Pass null to reset the handler to its default state. |
Return value
?callableThe previously defined exception handler, or null if none was defined or on error.
Changelog
- PHP 8.3.5The exception handler being invoked is pushed onto the handler stack and re-installed once it returns, unless the handler modified the stack itself.
- PHP 8.3.0The active exception handler is unset while it runs, so set_exception_handler() called from within a handler reports no previously defined handler.
set_exception_handler() installs the last line of defence: the function PHP calls when a Throwable reaches the top of the stack with nothing catching it. Since both Exception and Error implement Throwable, type the parameter as Throwable — a handler typed Exception will itself fail when a TypeError arrives.
The script still ends
This is the part people miss. After your handler returns, execution stops. You cannot resume the request, retry, or fall through to the rest of the script. Treat the handler as a place to log the failure and render a generic error page, nothing more. Anything that must run afterwards belongs in register_shutdown_function().
Write it defensively
If your handler throws, PHP reports a fatal error about an exception thrown inside the exception handler, and the original problem is lost. Keep it minimal: no database calls, no template engine, no autoloaded classes that might be missing. Wrap the risky part in its own try/catch.
Also remember it never sees caught exceptions, nor classic PHP errors — pair it with set_error_handler() for warnings and notices, and a shutdown function for fatal errors.
Example
When to use it
- Log uncaught exceptions to a crash reporter and show a generic 500 page
- Print a clean one-line failure message from a CLI script instead of a stack trace
- Guarantee that a long-running job records why it died before the process exits
Frequently asked questions
Does the script continue after set_exception_handler runs my handler?
No. PHP terminates the script once the handler returns; there is no way to resume. Use register_shutdown_function() for anything that must still happen.
Why does my exception handler not catch a TypeError?
Almost certainly because the handler's parameter is typed Exception. TypeError extends Error, so type the parameter as Throwable to receive both.
What is the difference between set_exception_handler and a try/catch block?
try/catch handles an exception where you expect it and lets the script continue, while the exception handler is a global fallback for exceptions nothing caught, after which the script ends.
More examples
Type the parameter as Throwable
intdiv() by zero throws DivisionByZeroError, which extends Error rather than Exception. Because the handler accepts Throwable it catches this too and reports it as an engine error; a handler typed Exception would have produced a TypeError instead.
<?php
set_exception_handler(function (Throwable $e): void {
$kind = $e instanceof Error ? 'engine error' : 'exception';
echo "{$kind}: ", $e::class, ' - ', $e->getMessage(), "\n";
});
echo "about to divide\n";
intdiv(1, 0); // throws DivisionByZeroError, an Error not an ExceptionHandler plus shutdown function
The output order is start, logged, cleanup. The exception handler runs first and then the script terminates, but shutdown functions still execute — which is why anything that must happen after a crash belongs there, not after the throw.
<?php
register_shutdown_function(function (): void {
echo "cleanup ran\n";
});
set_exception_handler(function (Throwable $e): void {
echo 'logged: ', $e->getMessage(), "\n";
});
echo "start\n";
throw new LogicException('bad state');Reading the previous handler
The return value gives you the handler you replaced, which is how code restored handlers before PHP 8.5. After a single restore the first handler is active again, confirming that handlers form a stack rather than a single slot.
<?php
$first = fn(Throwable $e) => print("first: {$e->getMessage()}\n");
$second = fn(Throwable $e) => print("second: {$e->getMessage()}\n");
var_dump(set_exception_handler($first)); // NULL - none before
var_dump(set_exception_handler($second) === $first); // bool(true)
restore_exception_handler();
var_dump(get_exception_handler() === $first); // bool(true)
Discussion