WEB Advent 2011 / Merry Error Handling

PHP does not enjoy the same consistency in error (and exception) handling as other languages, mostly due to historical reasons and the lack of a formal specification. But, there are things that you can do to make error handling saner and easier to maintain.

PHP has multiple categories of errors and exceptions, each with its own handling semantics within the PHP interpreter. For our purposes, we can classify these into the following categories:

  • Fatal errors, like out-of-memory and syntax errors, E_ERROR, E_CORE_ERROR, and E_PARSE.
  • User errors, including E_USER_ERROR, E_WARNING, E_PARSE, and all other non-fatal errors.
  • Exceptions (all subclasses of the Exception class).

Fatal errors are triggered from within the PHP interpreter and cannot be triggered by app code written in PHP. (It is possible, however, that some PHP extensions written in C may trigger fatal errors.)

User errors are largely deprecated in favor of exceptions, although they still get triggered by the PHP interpreter and some third-party libraries.

Exceptions behave the same way exceptions do in many other languages like Java, Python, and Ruby.

In an ideal world, all these categories would be condensed into one category, so you could apply the same rules and syntax to all of them. However, as you might have guessed, we do not live in an ideal world. Nonetheless, we will try to approximate it.

Fatal errors

There is a lot of literature out there about how to handle fatal errors using shutdown functions. Basically, set_error_handler() will not work for fatal errors. Instead, you can use register_shutdown_function() and pass it a function that performs some custom actions when fatal errors occur.

The problem is that, by the time the shutdown function is called, the call stack is emptied, and you have no useful backtrace to work with, except for where in the file the error has occurred. If you try to call debug_backtrace() in your shutdown function, it will return an empty array.

One way to work around this is to log a global variable in the shutdown function that can help debug the problem. This global variable could be the session identifier, user identifier, or anything that can help correlate log lines produced by the shutdown function with other log lines produced within the normal run of the app.

Furthermore, following some examples, calling error_get_last() means if any additional errors or warnings occur before the shutdown function is called, these will overwrite the error that actually caused the code to halt. Sadly, there is no way to avoid this, except to make sure these errors do not happen in the first place. These usually happen if PHP is incorrectly configured, or if you use poorly implemented third-party modules or C extensions that throw errors on shutdown.

Unifying user errors and exceptions

Much literature also exists about handling user errors, including in the PHP manual. However, most of the error handling solutions around suffer from two drawbacks:

  • You have to maintain two separate handlers for errors and exceptions.
  • You still cannot handle PHP errors like exceptions locally in the code. Instead, you have to rely on a global error handler.

The first drawback can be worked around by having both handlers simply call a central error handling facility instead of duplicating code. The following trick to wrap PHP errors as exceptions can be used to ameliorate the second drawback:

<?php

// These should usually be in your php.ini, but I'm putting it here to
// make sure this demonstration is portable across different configurations
ini_set('display_errors', 'stdout');
error_reporting(E_ALL);

function handle_error($errno, $errstr, $errfile, $errline) {
    // Note that you could also check for a larger set of PHP errors like:
    // $is_halting_error = $errno & (E_USER_ERROR | E_WARNING | E_USER_WARNING);
    $is_user_error = $errno & E_USER_ERROR;
    if ($is_user_error) { 
        throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    } else {
        // do some logging, custom actions, or even ignore the error
    }
}

set_error_handler('handle_error');

function test_fake_error() {
    trigger_error('Test error', E_USER_ERROR);
}

function test_caught_error() {
    try {
        test_fake_error();
    } catch (Exception $e) {
        print "I caught the exception with message: " . $e->getMessage() . "\n";
    }
}

function test_uncaught_error() {
    test_fake_error();
}

test_caught_error();
test_uncaught_error();

If you run the program above with a PHP 5.3 interpreter, then something like this should be on the standard output:

I caught the exception with message: Test error

Fatal error: Uncaught exception 'ErrorException' with message 'Test error' in test.php:22
Stack trace:
#0 [internal function]: handle_error(256, 'Test error', 'test.php', 22, Array)
#1 test.php(22): trigger_error('Test error', 256)
#2 test.php(34): test_fake_error()
#3 test.php(38): test_uncaught_error()
#4 {main}
  thrown in test.php on line 22

As you can see from this example, you can take advantage of both localized try/catch facilities as well as a full stack trace that comes for free from wrapping the error as an exception!

As an aside, you may notice something strange in the stack trace above; it looks as if the error handling function was called from inside the function that triggered the error. This is because, behind the scenes, what trigger_error() is really doing is simply calling error handlers with information about the user-defined error. If there is a user-defined error handler, then that is called first before calling the default error handler. (For details, see trigger_error() and zend_error().) The call from trigger_error() to the user-defined error handler is done using the call_user_function_ex() C function (which happens to be the same C function used in PHP’s call_user_func()), which records the call in the stack trace and leads to the backtrace seen above.

Developer Gift

Look for those unsung heroes in your workplace, the ones who keep things humming along without anyone ever noticing, simply because they are so good at what they do. Send them an email, ask to accompany them for a day or two, and put yourself in their shoes. It will most likely be an enlightening experience for you — a chance for you to explore what you have come to take for granted. Your interest in what they do will be a rare acknowledgement — a gift! — of how critical they are.

Other posts