skip to content

How do you turn PHP warnings and notices into exceptions with set_error_handler and ErrorException?

level: middleimportance: must knowfreq 50%

answer

  1. callback gets errno, errstr, errfile, errline
  2. check error_reporting() & $errno first
  3. return false: the standard handler runs
  4. fatal and compile errors never arrive
  5. getSeverity() keeps the E_* level

basics

~20 s

Register a callback with set_error_handler() that throws new ErrorException($errstr, 0, $errno, $errfile, $errline). Warnings and notices then become catchable exceptions; the callback should first skip levels excluded by error_reporting(), and fatal or compile-time errors never reach it.

solid answer

~40 s

`set_error_handler(?callable $callback, int $error_levels = E_ALL)` replaces PHP's standard handling of warnings, notices and deprecations with your function, which receives `int $errno, string $errstr, string $errfile, int $errline`. Throwing `new ErrorException($errstr, 0, $errno, $errfile, $errline)` from it turns, say, a failed `file_get_contents()` warning into an exception that `try`/`catch` can handle, with the original level available from `getSeverity()`. Two details matter. The callback runs even for levels that `error_reporting` excludes and for expressions silenced with `@`, so it must start with `if (!(error_reporting() & $errno)) { return false; }`. And returning `false` hands the error back to PHP's standard handler, while any other return marks it handled and the script continues after the offending statement. `E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_CORE_WARNING`, `E_COMPILE_ERROR` and `E_COMPILE_WARNING` never reach the callback.

code

php · 17 lines
php
<?php
declare(strict_types=1);

set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline): bool {
    if (!(error_reporting() & $errno)) {
        return false; // level not reported, or silenced with @
    }
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

try {
    $rates = file_get_contents('/etc/booking/rates.json');
} catch (ErrorException $e) {
    // "file_get_contents(/etc/booking/rates.json): Failed to open stream: No such file or directory"
    error_log($e->getMessage() . ' severity=' . $e->getSeverity());
    $rates = '{}';
}

go deeper

for a junior

Recall that set_error_handler() installs your function for warnings and notices, and that throwing ErrorException from it turns them into exceptions.

for a middle

Explain the callback's parameters and return values, the error_reporting() & $errno guard, and which error levels can never reach the handler.

for a senior

Install the conversion once at bootstrap, keep fatal errors covered through logging and error_get_last(), and scope temporary handlers with restore_error_handler() in finally.

for a principal

Decide whether a codebase treats every warning as an exception, and how libraries should coexist with the application's handler instead of replacing it.

## Why convert warnings into exceptions Many PHP functions report failure the old way: they return `false` (or `null`) and raise a **warning**, an `E_WARNING` diagnostic that is logged or displayed and then ignored by the program. `file_get_contents()` on a missing file, `fopen()` on an unwritable path and `mkdir()` on an existing directory all behave like this. If the caller forgets to check the return value, the script carries on with `false` where it expected data. Converting diagnostics into exceptions makes such failures **stop the current operation** unless something catches them, and lets you use one mechanism — `try`/`catch` — for every failure. Most frameworks install exactly this handler at bootstrap. ## The handler contract `set_error_handler(?callable $callback, int $error_levels = E_ALL)` installs the callback and returns the previously installed one (or `null`). | Part | Meaning | |---|---| | `int $errno` | The level, such as `E_WARNING` or `E_USER_DEPRECATED` | | `string $errstr` | The message; for internal functions it starts with the function name | | `string $errfile`, `int $errline` | Where the diagnostic was raised | | return `false` | PHP's standard handler runs as if no callback existed | | any other return | The error counts as handled; execution continues after the statement | | `$error_levels` | Only these levels are routed to the callback; the rest use the standard handler | A fifth `$errcontext` argument existed in old versions; it was removed in PHP 8.0, and a callback that still requires it fails with too few arguments. ## A correct handler The PHP manual is explicit that `error_reporting` settings do **not** stop the callback from being called. It is invoked for levels the configuration excludes and for expressions prefixed with `@`. So the first line must re-apply the filter: 1. `error_reporting()` returns the current level (reduced while an `@` expression runs). 2. `error_reporting() & $errno` is zero when this level should be ignored. 3. In that case return `false`, and PHP's standard handler also ignores it. Only then throw. `ErrorException` is the built-in class for this job: its constructor is `(string $message = "", int $code = 0, int $severity = E_ERROR, ?string $filename = null, ?int $line = null, ?Throwable $previous = null)`, and `getSeverity()` returns the level. Passing `$errfile` and `$errline` makes the exception report the line that raised the warning, not the line of the handler. Note that `$previous` is the **sixth** argument here, not the third as in `Exception`. ## What the handler never sees - **Fatal and compile-time errors**: `E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_CORE_WARNING`, `E_COMPILE_ERROR`, `E_COMPILE_WARNING`. Exhausting `memory_limit` or exceeding `max_execution_time` ends the request without calling it; the last error is readable afterwards with `error_get_last()` from a shutdown function. - **Errors raised before the script runs**, such as problems while PHP processes a file upload, because no handler is registered yet. - **Exceptions** — `TypeError`, `ValueError` and the rest are already throwables and go straight to `catch` blocks. ## Stacking and restoring Handlers form a stack. `set_error_handler()` pushes, `restore_error_handler()` pops back to the previous one, and passing `null` makes PHP use its standard handler while keeping the old one on the stack. PHP 8.5 added `get_error_handler()`, which returns the active callback (or `null`) without replacing it — useful for libraries that want to wrap, not clobber, whatever the application installed. ## Scoping a handler to one call A short-lived handler is a clean way to make **one call** throw without changing global behaviour: install, call, and restore in a `finally` block. ```php set_error_handler(static function (int $errno, string $errstr): never { throw new ErrorException($errstr, 0, $errno); }); try { $ok = mkdir($bookingDir, 0775, true); } finally { restore_error_handler(); } ``` The `finally` guarantees that the previous handler comes back whether `mkdir()` succeeded or threw. Library code should prefer this pattern over a permanent global handler, because the application that uses the library owns global error policy. A handler declared with a `never` return type must always throw, which documents that this one never lets a warning through.

  • Why must the handler check error_reporting() & $errno before throwing?
    Because PHP calls a user error handler even for levels that `error_reporting` excludes and for expressions prefixed with `@`. Without the check, a deprecation you chose not to report, or a deliberately silenced call, would throw. Returning `false` when the bit is not set leaves such errors to PHP's standard handler, which ignores them.
  • Can set_error_handler() catch a request dying from memory exhaustion?
    No. Exhausting `memory_limit` raises `E_ERROR`, one of the fatal levels a user error handler never receives. The request ends, and the only record is PHP's own log line plus whatever a shutdown function reads from `error_get_last()` afterwards.
  • Where does the $previous argument go in the ErrorException constructor?
    It is the sixth parameter: `new ErrorException($message, $code, $severity, $filename, $line, $previous)`. The extra severity, file and line parameters push it back from the third position it has in `Exception`, so a named argument (`previous: $e`) is the safer way to pass it.

saying these in an interview costs you the question

  • error_reporting settings stop the custom handler from being called
  • A user error handler can also catch E_ERROR fatal errors
  • Returning true from the handler makes PHP log the error as usual
  • set_error_handler also receives thrown TypeError and ValueError objects
  • The ErrorException constructor takes $previous as its third argument