Custom Error Handler
What Is a Custom Error Handler? In simple terms: By default, PHP prints or logs warnings and notices and then continues. A custom error handler is your own function that PHP calls instead whenever suc
What Is a Custom Error Handler?
In simple terms: By default, PHP prints or logs warnings and notices and then continues. A custom error handler is your own function that PHP calls instead whenever such an error occurs, so you decide what happens: log it in a specific format, ignore it, or turn it into an exception that stops the faulty operation.
Technically: set_error_handler() registers a callable that receives the error's severity, message, file, and line. It is invoked for warnings, notices, deprecations, and user-triggered errors (trigger_error()). It is not invoked for fatal errors (E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR), which are handled with a shutdown function. Together with set_exception_handler() and register_shutdown_function(), it forms a complete error-handling setup.
Why Use One?
- Fail fast: Converting warnings into exceptions means a missing file or an undefined array key stops the operation immediately, instead of continuing with
falseornulland causing confusing errors later. - Consistency: All problems — warnings, exceptions, fatal errors — flow into one logging and reporting path.
- Control: Deprecations can be collected and logged separately, without disturbing users.
Syntax
set_error_handler(?callable $callback, int $error_levels = E_ALL): ?callable
The callback's signature:
function (int $severity, string $message, string $file, int $line): bool
| Return Value | Meaning |
|---|---|
true | The error is handled; PHP's standard handler does not run |
false | PHP's standard handler runs as well (displaying/logging the error normally) |
restore_error_handler() reactivates the previous handler.
Converting Errors Into Exceptions
The most common custom handler throws an ErrorException, a built-in exception class designed for this purpose:
<?php
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
if (!(error_reporting() & $severity)) {
return false; // this error type is not reported (or was silenced with @)
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
try {
$config = file_get_contents('/no/such/config.json');
} catch (ErrorException $e) {
echo "Caught: " . $e->getMessage() . "\n";
}
try {
$items = ['a'];
$item = $items[3];
} catch (ErrorException $e) {
echo "Caught: " . $e->getMessage() . "\n";
}
$result = @file_get_contents('/no/such/file');
var_dump($result);
Output
Caught: file_get_contents(/no/such/config.json): Failed to open stream: No such file or directory
Caught: Undefined array key 3
bool(false)
How It Works
- When PHP raises a warning, it calls the handler instead of printing the warning.
error_reporting() & $severitychecks whether this type of error is currently reported. When an expression is prefixed with@, PHP 8 temporarily reduceserror_reporting()to fatal error types only, so the check fails and the handler returnsfalse— respecting the@operator, as the last example shows.- Otherwise, the handler throws an
ErrorExceptionthat carries the severity, file, and line of the original error. - The calling code can now catch these problems with
try/catch, just like any exception.
With this handler in place, file_get_contents() either returns the content or throws — there is no silent false to forget about.
Note: Frameworks such as Laravel and Symfony install a handler like this by default, which is why warnings appear as exceptions with stack traces in those frameworks.
Handling Deprecations Differently
Throwing on every error type can be too strict: a deprecation notice in a third-party library should not crash a page. A handler can treat severities differently:
<?php
$deprecations = [];
set_error_handler(function (int $severity, string $message) use (&$deprecations): bool {
if ($severity === E_DEPRECATED || $severity === E_USER_DEPRECATED) {
$deprecations[] = $message;
return true;
}
return false;
});
trigger_error('oldPrice() is deprecated; use price() instead', E_USER_DEPRECATED);
$length = strlen(null);
restore_error_handler();
print_r($deprecations);
Output
Array
(
[0] => oldPrice() is deprecated; use price() instead
[1] => strlen(): Passing null to parameter #1 ($string) of type string is deprecated
)
trigger_error()withE_USER_DEPRECATEDlets your own code announce deprecations, for example in a shared library.- The handler collects deprecations instead of displaying them; in a real application, it would write them to a separate log so developers can fix them before the next PHP upgrade.
- Returning
falsefor other severities lets PHP handle them normally. restore_error_handler()removes this handler again — useful when a handler is needed only temporarily.
Readable Severity Names
<?php
function severityName(int $severity): string
{
return match ($severity) {
E_WARNING, E_USER_WARNING => 'Warning',
E_NOTICE, E_USER_NOTICE => 'Notice',
E_DEPRECATED, E_USER_DEPRECATED => 'Deprecated',
E_RECOVERABLE_ERROR => 'Recoverable error',
default => 'Error',
};
}
echo severityName(E_USER_DEPRECATED) . "\n";
echo severityName(E_WARNING) . "\n";
Output
Deprecated
Warning
A Complete Error-Handling Bootstrap
The following file sets up error handling for a whole application. It is included at the very beginning of every request (for example, in public/index.php):
<?php
// src/bootstrap-errors.php
declare(strict_types=1);
$isDevelopment = (getenv('APP_ENV') ?: 'production') === 'development';
error_reporting(E_ALL);
ini_set('display_errors', $isDevelopment ? '1' : '0');
ini_set('log_errors', '1');
// 1. Warnings, notices, and deprecations
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
if (!(error_reporting() & $severity)) {
return false;
}
if ($severity === E_DEPRECATED || $severity === E_USER_DEPRECATED) {
error_log("Deprecated: $message in $file:$line");
return true;
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
// 2. Uncaught exceptions and Errors
set_exception_handler(function (Throwable $e) use ($isDevelopment): void {
error_log(sprintf("%s: %s in %s:%d\n%s",
get_class($e), $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString()));
if (!headers_sent()) {
http_response_code(500);
}
if ($isDevelopment) {
echo '<h1>' . htmlspecialchars(get_class($e)) . '</h1>';
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
echo '<pre>' . htmlspecialchars($e->getTraceAsString()) . '</pre>';
} else {
echo '<h1>Something went wrong</h1><p>Please try again later.</p>';
}
});
// 3. Fatal errors that cannot be caught
register_shutdown_function(function () use ($isDevelopment): void {
$error = error_get_last();
if ($error === null || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
return;
}
error_log("Fatal: {$error['message']} in {$error['file']}:{$error['line']}");
if (!headers_sent()) {
http_response_code(500);
}
if (!$isDevelopment) {
echo '<h1>Something went wrong</h1><p>Please try again later.</p>';
}
});
How the Three Parts Work Together
| Problem | Caught By | Result |
|---|---|---|
| Warning (missing file, undefined key) | Error handler → ErrorException | Can be caught locally; otherwise reaches the exception handler |
| Deprecation | Error handler | Logged; the request continues |
Uncaught exception or Error | Exception handler | Logged with stack trace; status 500; details only in development |
| Fatal error (memory, timeout) | Shutdown function | Logged; status 500 if possible |
Step-by-Step Explanation
- The environment (development or production) decides whether details are displayed. Errors are always reported and logged.
- The error handler converts warnings into exceptions and logs deprecations without interrupting the request.
- The exception handler is the final safety net for anything not caught elsewhere. All output is escaped, because exception messages can contain user input.
- The shutdown function catches the remaining fatal errors, which never reach the other handlers. In development, PHP has already displayed those errors itself, so only production output is added.
In production applications, the logging calls are usually replaced by a logging library such as Monolog, and errors may also be sent to monitoring services (see Error Logging).
Common Mistakes
Ignoring error_reporting() in the Handler
Effect: Errors suppressed with @, or excluded by configuration, still throw exceptions.
Fix: Start the handler with if (!(error_reporting() & $severity)) return false;.
Expecting the Handler to Catch Fatal Errors
Effect: Memory and timeout errors bypass it.
Fix: Use register_shutdown_function() with error_get_last().
Showing Exception Details to Users
Effect: File paths, queries, and secrets are exposed.
Fix: Show details only in development; log them in production.
Throwing on Deprecations From Third-Party Code
Effect: Pages break after a PHP upgrade because a library uses a deprecated feature.
Fix: Log deprecations instead of throwing.
Echoing Unescaped Messages
Effect: Error pages become an XSS vector.
Fix: Escape all output in handlers with htmlspecialchars().
Best Practices
- Install the error, exception, and shutdown handlers at the very start of each request.
- Convert warnings to
ErrorExceptionwhile respectingerror_reporting()and@. - Log deprecations separately and fix them regularly.
- Display detailed errors only in development; always log everything.
- Prefer the error handling provided by your framework, and understand how it works internally.
Official reference: https://www.php.net/manual/en/function.set-error-handler.php