Error vs Exception, Part 4

In this series, I’m going to take a look at both PHP’s error and exception models — what each is, how they work, and how and when to use the one or the other.

In this instalment, having covered the basics of PHP error handling in part 1, the need for exceptions in part 2, and how to work with exceptions in part 3, let’s take a look at error- and exception-handling methodology.

Context

The use of exceptions is linked to the fact that a given piece of code, like a method or function, only knows what task it is expected to perform, and is generally not aware of the context in which it finds itself operating.

Temet Nosce

Code doesn’t look up the call stack to see who called it and why. So when some higher level code, say a function, asks lower level code, say a class method, to perform a task and for some reason it just cannot perform that task at all, it has no authority to decide the direction that the entire program should then take as it has no understanding of the entire program, no understanding of the reason for it being called at all – it only has an understanding of itself and the job it has to do, not of its context. So when it utterly fails in its task, it should rather communicate upwards and tell the higher level code that called it, the ‘why‘ behind it failing.

Our only hope, our only peace, is to understand it, to understand the why.

As code higher up better understands a certain context than code lower down the call stack, it can respond more appropriately depending on said context. As example, think about a database class that is asked to connect to a database and retrieve some data: if it cannot connect to the database, what should happen? Should it steer the program somewhere? If so, where? Should your program end there, or not? If you think about it, you’ll see that it depends entirely on the context within which it was called – it might be critical to retrieve that data, but it might not be; it might be necessary to tell the user precisely what is wrong, but only if it’s an administrator during initial back-end setup, etc. – the database class itself just doesn’t know these things, it is not aware of the context in which it is being asked to do its task, and so it shouldn’t steer the program at all!

Of course. But this is not a reason. This is not a “why”.

The way that code should communicate the ‘why’ behind it not performing its task, back up the call stack, is by making use of exceptions, as explained in part 2. The ‘why’ is communicated in two ways especially: the type of exception, and then also the message which gets passed along with the exception to give more precise information :

throw new DatabaseConnectException("No password was given?");

When code that is called (lower down code) is not performing its task as instructed and communicates this upward as in the example above, the higher level code could even try something else, such as retrieving that data from another source. If that should also fail, and this higher level code also cannot perform its task at all, it should in turn communicate upwards as well.

try {
    $MyDB = new Database($username);
    $data = $MyDB->query($q);
} 
catch (DatabaseException $E) {
    try {
        // reaches here if database error
        $data = $MyFile->get();
    }
    catch (Exception $F) {
        // reaches here if file retrieve didn't work either
        // nothing more to do so communicate the fail upwards
        throw new MyDataException('Could not retrieve data.');
    }
}
// But if execution reaches here, a retrieve worked

If no exception is thrown in the first try, the entire outer catch will just get skipped and the program will continue. But if the first try doesn’t work, we move into the first catch. If that second try works, good, the program will skip the inner catch and just continue as well. But if it also doesn’t work, we move to the second catch which throws an exception that will return the program control to a catch higher up in the code.

As an aside, let me just quickly point out something here which perhaps isn’t completely clear yet: as I said in part 2, when an exception is thrown, control is immediately returned up the stack, to the first catch. In other words, in our example above, if line 2 ($MyDB = new Database($username)) throws an exception, line 3 ($data = $MyDB->query($q)) will not execute at all – the program execution immediately moves to the catch.

What if I can’t stop him?

As I also stated in part 2, a thrown exception which isn’t caught anywhere in your program will result in a Fatal Error – in other words, uncaught exceptions halt script execution, and so all exceptions have to be caught somewhere. When and where you catch exceptions depends on the context, as in the earlier example, so is program specific – you’ll have many different places in your program where code that instruct some lower level function to perform some task, will wrap that set of instructions in a trycatch block expecting a possible failure, to either then manage that failure in a different way, or communicate upward its failure to perform its own task because of it. But to avoid all chances of a Fatal Error, it is also a good idea to set a default exception handler that will catch any exceptions which have bubbled up and not been caught anywhere. You do this in a similar way to setting the default error handler as we did in part 1 :

set_exception_handler ( 'a_callback_function' );

There is no $error_type_bitmask as with setting the default error handler, as the default exception handler responds to ALL uncaught exceptions, not only certain types.

On a live site, you set the display_errors directive to false as I also explained in part 1. So what both the default exception and error handlers might do then, is show a ‘technical problems’ page to the users instead, and notify you as developer or site admin of the problem. Whether an email should be sent to you when an exception or error is caught by the default handlers, probably mostly depends on the size of the site – for a small site (in terms of the number of users who visit the site), receiving an immediate email is great. But for larger sites, that’s not really an option, as you’d receive hundreds of emails from your application in a matter of minutes if something were to go wrong. In that case, you’d be better off setting up a cron job to check your log file say every 5 minutes and send you an email if there are new entries – that way, you’ll receive a limited number of emails per hour should something go wrong.

Now there’s more than one of him?

In most applications, the default error and exception handlers will end up doing nearly the same thing, as described above. Unlike men though, not all errors are created equal, and so not all errors have to halt the program execution. Assuming that your default error handler has been set to respond to all errors as suggested in part 1 (error_reporting(-1)), it could log all errors, but then let the program execution continue for errors of say type E_USER_WARNING, E_USER_NOTICE, E_NOTICE and E_STRICT, as these aren’t serious. For the rest of the errors, instead of duplicating the functionality of displaying a ‘technical problems’ page to the user, one could let your custom error handler throw what is called an ErrorException instead – it is just another type of exception, but comes predefined as part of the PHP 5+ core, and carries with it the meaning of an error in the code which has been ‘converted’ in this way to an exception instead. This has another benefit too, which I’ve explained in another article, namely that some PHP code like the fopen function can generate an error when it should be throwing an exception instead, and by having your default error handler covert errors to exceptions (excluding the ones listed earlier for which program execution just continues), even that E_WARNING error could be managed by your application in a better way. Errors, changed now to exceptions, will then, unless explicitly caught by a trycatch block in your application, be picked up by the default exception handler, which could then respond with the ‘technical problems’ page, etc.

…there’s so many of me.

The Exception and ErrorException classes aren’t the only predefined exceptions in PHP, there are a number of others in the SPL, which is a collection of interfaces and classes that are meant to solve standard problems. Look under exceptions in the PHP manual entry for the SPL. It’s a good idea to understand these and use them as much as is possible, as having a standardized palette of exceptions to draw with means that your code is easier for other developers – as well as yourself when you return to a piece of code months later – to immediately understand when reading it. In other words, your code becomes more habitable in the Christopher Alexander sense – see Richard Gabriel’s Patterns of Software: Tales from the Software Community for more.

Death can come for us at any time, in any place.

One last type of situation we should be ready to respond to, is when our application has shut down unexpectedly. We can do this by registering a shutdown function with, you guessed it, register_shutdown_function(). It automatically gets called by PHP at the end of script execution, whether unexpected or not, and should check if an error has occurred and respond if necessary. This will then manage errors that aren’t picked up by the default error handler.

I know Kung Fu.

A default error handler, incorporating what I wrote in How to ignore @ errors in a custom PHP error handler, might then look as follows:

static function handle_error ($level, $message, $file, $line) {

    // ignore @-suppressed errors
    $error_is_enabled = (bool)($level & (int) ini_get('error_reporting') );
    if ( !$error_is_enabled ) {
        return false;
    }
    
    // throw ErrorException for any error not in my $continue array
    if (!in_array($level, static::$continue)) {
        throw new ErrorException($message, $level, 0, $file, $line);
    }

    // make entry more human-readable
    $message = static::message($level, $message, $file, $line);

    // if in DEBUG mode, print error to screen
    if (DEBUG) {
        if (ob_get_level() > 0) ob_clean();
        echo $message;
        exit;
    }
    // else log to location set earlier with ini_set
    else {
        $location = ini_get('error_log');
        error_log($message, 3, $location);
    }
    // program execution continues
}

and a default exception handler might then look like this:

static function handle_exception ($E) {

    if (ob_get_level() > 0) ob_clean();

    // make entry more human-readable
    $message = static::message($E->getCode(), $E->getMessage(), $E->getFile(), $E->getLine());

    // if in DEBUG mode, print error to screen
    if (DEBUG) {
        echo $message;
    }
    // else log to location set earlier with ini_set
    else {
        $location = ini_get('error_log');
        error_log($message, 3, $location);
    }

    // respond to user here, such as a 'technical problems' page
    require ('error_technical.php');

    // program execution automatically ends here
}

Here then is (part of) the way in which I set up my error handling, exception handling and shutdown functions, using PHP 5.3+ :

error_reporting(-1);
$error_log = PATH_LOG . date('Y-m-d', time()) . '_error.log';
ini_set('error_log', $error_log);
if (DEBUG) {
    ini_set('display_errors', 1);
    ini_set('log_errors', 0);		
}
else {
    ini_set('display_errors', 0);
    ini_set('log_errors', 1);
}

// Error Handler
set_error_handler(function ($level, $message, $file, $line) {
    if (!class_exists('\\Hive\\Error', false)) {
        require (PATH_CORE.'error.core.php');
    }
    Error::handle_error($level, $message, $file, $line);
}, (int) ini_get('error_reporting'));

// Exception Handler
set_exception_handler(function ($E) {
    if (!class_exists('\\Hive\\Error', false)) {
        require (PATH_CORE.'error.core.php');
    }
    Error::handle_exception($E);
});

// Shutdown Function
register_shutdown_function(function () {
    if ( ! is_null($error = error_get_last())) {
        if (!class_exists('\\Hive\\Error', false)) {
            require (PATH_CORE.'error.core.php');
        }
        Error::handle_exception(new \ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line']));
    }    
});

Unfortunately, no one can be told…

I have tried to give you a conceptual understanding of errors and exceptions in this series, and hope that I have succeeded. I have also provided you with code to help you understand the concepts. However, the best way to gain a thorough understanding is by doing it yourself, so I hope that you’ll create a sandbox and do some experiments – set up handlers, trigger errors, throw exceptions, and study what happens, you’ll be sure to learn a lot more!

Everything that has a beginning has an end.

That’s it for this series, thank you for reading. As always, your comments and questions are most welcome.

I will say adieu and goodbye..

6 Responses to “Error vs Exception, Part 4”

  1. […] View post:  Error vs Exception, Part 4 – Anvil Studios […]

  2. Mike says:

    I thoroughly enjoyed this whole series and I’m definitely going to be putting it into use in some of my scripts. Thanks for taking the time to write it.

    • Abraham says:

      Mike, I’m glad you enjoyed it and found it useful as well! Thanks for the encouragement earlier to continue with the series, and for taking the time to leave a comment. Kanpai!

  3. Isius says:

    Abraham,
    Once again I’ve been thoroughly impressed with these articles and have gained excellent “best practice” knowledge that I can use in my projects.

    Many thanks to you sir!
    Isius

  4. Flink says:

    Not many comments for such an important topic. You’ve done a great job, thank you very much. I’m still thinking about the best way for a clean, generic, reusable exception handling. But like you said, better solutions come with more practice! Once more, thanks for your time spent.

Leave a Reply to Abraham