Streamlined error handling in PHP

Table of contents

When coming from other languages, developers are often used to having a single type for errors, for example Exceptions in object-oriented languages. PHP is different in this regard, as there is a total of three error types that must be caught at runtime, with special settings to adjust their behavior.

Uncaught exceptions

The obvious errors are likely exceptions, as they are thrown from working code to indicate a problem. While they can be caught in place with a try...catch block, failing to catch one will stop script execution with this message:

Fatal error: Uncaught Exception: test in /usr/src/myapp/index.php:3

To mend this problem, PHP allows setting a global exception handler that will catch any exceptions thrown outside of a try...catch block using set_exception_handler():

<?php

    function handleException(Throwable $e){
        echo "Caught exception:\n";
        var_dump($e);
        die(); // end execution
    }
    set_exception_handler(handleException(...));

The function handleException() will now be called instead of printing an error every time an exception is thrown and not caught. It is important that this code appears at the beginning of the script to ensure every possible exception is correctly forwarded to the handler function. This function may then deal with the exception, for example by triggering an alert or writing it to a log file.

Since we are planning to forward all errors to the handleException() function, we have declared it as a named function instead of passing a closure to set_exception_handler(), and we will also not be able to recover from errors, so only cleanup, logging and alerting is allowed in this function.

Catching errors

The second type of script-halting issue are errors. Errors are triggered by issues that are considered too severe to recover from, for example trying to reference a variable $bad that does not exist will result in this error:

Warning: Undefined variable $bad in /usr/src/myapp/index.php on line 8

To streamline errors into the exceptionHandler(), we can first use set_error_handler() to catch it, and throw it as an ErrorException. This type of exception is specifically built into PHP to convert errors into Exceptions:

<?php

    set_error_handler(function($severity, $message, $file, $line){
        throw new ErrorException($message, 0, $severity, $file, $line);
    });

While this may work, it has a problem: It will catch all errors and throw them as ErrorExceptions, without even when the error_reporting levle is set to ignore them. To fix this, we can include a quick check to see if $severity is reportable using error_reporting():

<?php

    set_error_handler(function($severity, $message, $file, $line){
        if (!(error_reporting() & $severity)){
            // This error code is not included in error_reportingreturn;
        }
        throw new ErrorException($message, 0, $severity, $file, $line);
    });

Now only errors of a level configured as reportable are thrown as exceptions to be caught by handleException().

Catching fatal errors

There is one more type of error that can bypass both error- and exception handlers, so-called fatal errors. A fatal error will abort the script in-place even with error handlers configured. These errors are rare, but can occur occasionally. They are usually caused by system-side errors like exceeding the maximum execution time for scripts, or exhausting the memory limit. Trying to use too much memory with $memory = str_repeat('a', PHP_INT_MAX); would result in this error:

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 9223372036854775839 bytes) in /usr/src/myapp/index.php on line 16

There is no direct function to deal with fatal errors, but we can instead register a function to run after the script execution has ended using register_shutdown_function() to catch these errors and redirect them to handleException() as well:

<?php

    register_shutdown_function(function(){
        $err = error_get_last();
        if($err != null){
            handleException(new ErrorException($err["message"], 0, $err["type"], $err["file"], $err["line"]));
        }
    });

Note that this function differs slightly from the error handler: We omit the check for reporting level as fatal is the highest possible level and cannot be turned off. Next we check if there even was an error using error_get_last(), because the shutdown function will always run after every script, whether it completed successfully or not. Lastly, we do not throw the ErrorException, but pass it as an argument to handleException() directly. We have to call it this way because when the shutdown function runs, the script (and any registered exception handlers) are already done executing, so they won't catch exceptions anymore at this point.

Preventing undesirable output

If you followed along with the above examples, you may have noticed that the error messages were still displayed before the error handlers caught and processed the errors. This behavior is controlled by the display_errors directive in the php.ini config. You can either change it there, or start your code by explicitly setting it to Off:

<?php

    ini_set('display_errors', 'Off');

The second type of output to prevent are partial responses. Consider this example, assuming $bad references a nonexistent variable:

<?php

    echo "a";
    echo $bad;
    echo "b";

The error will occur once the line trying to print $bad is reached, but at that point, the first echo command will have already printed a to the output. In context of HTTP responses, this can be even worse, since printing output will also send the HTTP headers, meaning an error page cannot be shown anymore as the HTTP status code was already sent (and part of the HTML page).

This can be prevented with output buffering. A call to ob_start() will keep output in a buffer instead of writing it directly, which can then either be flushed to the client or deleted and replaced with something entirely different. To make this work, output buffering has to be started at the beginning of the script, and the buffer needs to be cleared when handleException() is reached:

<?php

    ob_start();
    ob_implicit_flush(false);

    function handleException(Throwable $e){
        if(ob_get_contents()){
            ob_end_clean();
        }
        echo "Caught exception:\n";
        var_dump($e);
    }

We start output buffering before any other code runs, and deleted any partial buffered responses before handling the error in handleException(). Note the call to ob_implicit_flush(false); - this prevents the output buffering from flushing the buffer to the client automatically when a specific threshold of buffered content is reached.

The complete script

Here is the entire script needed to streamline all errors into a single handleErrors() function. This must appear before any other code in the script.

<?php

    error_reporting(E_ALL);
    ini_set('display_errors', 'Off');
    ob_start();
    ob_implicit_flush(false);

    function handleException(Throwable $e){
        if(ob_get_contents()){
            ob_end_clean();
        }
        echo "Caught exception:\n";
        var_dump($e);
    }

    set_exception_handler(handleException(...));

    set_error_handler(function($severity, $message, $file, $line){
        if (!(error_reporting() & $severity)){
            // This error code is not included in error_reportingreturn;
        }
        throw new ErrorException($message, 0, $severity, $file, $line);
    });

    register_shutdown_function(function(){
        $err = error_get_last();
        if($err != null){
            handleException(new ErrorException($err["message"], 0, $err["type"], $err["file"], $err["line"]));
        }
    });

    // real script logic starts here



The different kinds of errors appearing in PHP scripts can be tricky to handle, especially when coming from other languages, where usually only one kind of error exists. With a little setup code, handling all sorts of errors that can occur can be streamlined into a single function working with a simple Throwable (aka Exception instance).

More articles

An introduction to structured logging

Bringing order to log messages

Builtin HTTP routing in Go

Fully featured web services with standard library only

Operating postgres clusters on Kubernetes

Automating backups and failover with the kubegres operator

The downsides of source-available software licenses

And how it differs from real open-source licenses

Configure linux debian to boot into a fullscreen application

Running kiosk-mode applications with confidence

How to use ansible with vagrant environments

Painlessly connect vagrant infrastructure and ansible playbooks