In PHP, what does set_exception_handler() do, and what happens to the script after the handler has run?
answer
- last stop for uncaught throwables
- gets Exception and Error alike
- the script ends after it returns
- no automatic 500 or log line
- returns the previous handler
basics
~20 sset_exception_handler() registers a callback for any Throwable that no catch block handled. PHP unwinds the stack, runs pending finally blocks, calls the handler with the object and then ends the script; execution never resumes at the throw site.
solid answer
~50 sWhen a throwable escapes every `try`, PHP runs the `finally` blocks on the way out and then, if `set_exception_handler()` registered a callback, calls it with the object — `Exception` and `Error` alike, so the parameter should be typed `Throwable`. After the handler returns the script ends; shutdown functions and destructors still run, but nothing resumes at the throw site. Without a handler, PHP reports `Fatal error: Uncaught ...` at `E_ERROR` level: logged, displayed if `display_errors` is On, status 500 if nothing was output yet, exit status 255 on the CLI. A handler replaces that report, so it must do the work itself: log the throwable (its string form includes the whole `previous` chain), call `http_response_code(500)` while headers are unsent, and render a safe page. The function returns the previous handler, and passing `null` restores the default.
code
php · 13 lines<?php
declare(strict_types=1);
set_exception_handler(static function (Throwable $e): void {
error_log('[booking] ' . $e); // class, message, trace and previous chain
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: text/html; charset=utf-8');
}
echo '<h1>Something went wrong</h1><p>Your booking was not changed. Please try again.</p>';
});go deeper
Recall that set_exception_handler() runs for exceptions no catch handled, and that the script ends after it.
Explain what PHP does for an uncaught throwable without a handler — E_ERROR, log, 500, exit 255 — and why a handler must log and set the status itself.
Write a defensive global handler that logs the full chain, sets 500 while headers are unsent, renders no internals, and pairs with fatal-error capture.
Define one failure path for the application — error handler, exception handler and fatal capture — so every failure is logged once and answered consistently.
## Where the handler sits An exception handler is the **last stop** for a throwable nothing else caught. The sequence for an uncaught exception is: 1. The throwable propagates up the call stack, and every `finally` block on the way runs. 2. It leaves the outermost code of the script without meeting a matching `catch`. 3. If a handler is registered, PHP calls it with the throwable as its only argument. 4. When the handler returns, the script ends. Registered shutdown functions run and remaining objects are destroyed, but execution never goes back to where the exception was thrown. The handler receives every kind of throwable — application exceptions, `TypeError`, `ValueError`, `DivisionByZeroError` — so its parameter should be typed `Throwable`, not `Exception`. It is also called for an exception that escapes a shutdown function, as php-src's tests show. ## Without a handler When no handler is registered, PHP turns the uncaught throwable into an `E_ERROR` fatal error with the message `Uncaught ...` followed by the exception's string form, including its trace and any chained previous exceptions. That fatal error then goes through the normal reporting machinery: - it is **logged** if `log_errors` is On; - it is **displayed** if `display_errors` is On; - the HTTP status becomes **500** if nothing had been output yet and display is off; - a CLI script exits with status **255**. ## With a handler: what becomes your job Registering a handler means PHP no longer raises that fatal error, so none of the defaults above happen. The handler must supply them: | Concern | Default without a handler | What the handler must do | |---|---|---| | Logging | `Uncaught ...` written to the error log | Log the throwable, e.g. `error_log((string) $e)` or an application logger | | Status code | 500 set by PHP when possible | Call `http_response_code(500)` if `headers_sent()` is false | | Output | Error text if display is on, otherwise nothing | Render a generic page or JSON body with no internals | | Exit code (CLI) | 255 | Call `exit(1)` or another non-zero code if scripts depend on it | Forgetting the first two rows produces the worst failure mode: an empty 200 response and an empty log. ## Handler pitfalls - **An exception inside the handler** is not passed back to it. Since PHP 8.3 the engine unsets the handler while it runs, so a bug in the handler surfaces as its own failure. Keep handlers small and defensive. - **Changing the handler from inside itself** is discouraged by the manual; the restore rules are intricate. - **Fatal errors are not throwables.** Running out of memory or time never reaches this handler; those need logging plus a shutdown function reading `error_get_last()`. - **Displaying `$e->getMessage()`** to users leaks internals just as `display_errors` would. ## Managing handlers `set_exception_handler()` returns the previously registered callback, or `null`. Passing `null` resets PHP to its default reporting, and `restore_exception_handler()` goes back to the previous one. PHP 8.5 added `get_exception_handler()`, which returns the active handler without replacing it. Applications normally install one handler at bootstrap, next to a `set_error_handler()` that turns warnings into `ErrorException`, so every failure — warning or exception — ends in the same logging and rendering path. ## Handler versus a top-level catch A front controller often wraps request handling in its own `try`/`catch (\Throwable $e)`, and many applications render their error pages there. The global exception handler is still worth installing, because some code runs **outside** that `try`: - bootstrap code that runs before the front controller's `try` is entered — loading configuration, building the service container; - shutdown functions and destructors that run after the response; - CLI scripts and queue workers that do not go through the web front controller. The top-level `catch` and the handler should share one reporting function, so a failure logs the same way wherever it is caught. The handler is the safety net; it should rarely fire, and every time it does is worth an alert.
- Does a handler registered with set_exception_handler() receive TypeError and other Error subclasses?Yes. It receives any uncaught `Throwable`, and `Error` subclasses such as `TypeError` and `ValueError` are the most common thing to arrive there, because they usually signal bugs no `catch` expected. Type the parameter as `Throwable`; typing it `Exception` makes the call itself fail with a `TypeError` when an `Error` arrives.
- What does set_exception_handler() return, and how do you undo it?It returns the previously registered handler, or `null` if there was none. Passing `null` makes PHP fall back to its default uncaught-exception reporting, and `restore_exception_handler()` reinstates the previous handler. PHP 8.5 also added `get_exception_handler()` to read the active handler without changing it.
saying these in an interview costs you the question
- After the handler returns, execution resumes after the throw
- The handler only receives Exception subclasses, never Error
- PHP still logs the uncaught exception and sends 500 when a handler exists
- set_exception_handler() also catches out-of-memory fatal errors
- Showing $e->getMessage() to users is a safe error page