Custom Error Handler

Su Subhalakshmi Chandran Updated 17 Oct 2026
6 min read ·Lesson 97 of 109
In this post

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 false or null and 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 ValueMeaning
trueThe error is handled; PHP's standard handler does not run
falsePHP'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

  1. When PHP raises a warning, it calls the handler instead of printing the warning.
  2. error_reporting() & $severity checks whether this type of error is currently reported. When an expression is prefixed with @, PHP 8 temporarily reduces error_reporting() to fatal error types only, so the check fails and the handler returns false — respecting the @ operator, as the last example shows.
  3. Otherwise, the handler throws an ErrorException that carries the severity, file, and line of the original error.
  4. 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() with E_USER_DEPRECATED lets 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 false for 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

ProblemCaught ByResult
Warning (missing file, undefined key)Error handler → ErrorExceptionCan be caught locally; otherwise reaches the exception handler
DeprecationError handlerLogged; the request continues
Uncaught exception or ErrorException handlerLogged with stack trace; status 500; details only in development
Fatal error (memory, timeout)Shutdown functionLogged; status 500 if possible

Step-by-Step Explanation

  1. The environment (development or production) decides whether details are displayed. Errors are always reported and logged.
  2. The error handler converts warnings into exceptions and logs deprecations without interrupting the request.
  3. The exception handler is the final safety net for anything not caught elsewhere. All output is escaped, because exception messages can contain user input.
  4. 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 ErrorException while respecting error_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

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators
Ask about this post
AI Ask about this post

Ask questions about Custom Error Handler and get answers drawn from it.

Signed-in readers only.