Keep Errors out of PHP

PHP has a number of faults. One of them being that it has two different models for handling errors. It's frustrating to setup exception handling in your application only to find out that an error is being thrown and not getting caught.

Warning: Division by zero in /Users/garrett/.../test.php on line 32

You can regard these error messages as garbage (to be hidden at the first opportunity), or you can view them as meaningful scenarios that expose a need for code changes. The latter being the better solution, the former being one that is easily supported by just hiding all errors. Luckily, capturing these errors is not out of the question.

PHP provides a mechanism to hook into the error handler (quaintly named set_error_handler()), which can allow us to kick off an exception. We'll discuss whether this is a good idea in a moment, but for now let's consider the execution. The following will give use some basic error translation.

function translate_error_to_exception($errno, $msg, $file, $line) {
  throw new ErrorException( $msg, 0, $errno, $file, $line);
  return false;
}
set_error_handler( "translate_error_to_exception", E_ALL^E_NOTICE );

This is good, but we also need something to handle some of the more unusual errors too.

function translate_shutdown_error_to_exception() {
  $error = error_get_last();
  switch($error['type']) {
    case E_ERROR:
    case E_CORE_ERROR:
    case E_COMPILE_ERROR:
    case E_USER_ERROR:
      throw new ErrorException( $error['message'], 0, $error['type'],
                                $error['file'], $error['line']);
  }
}
register_shutdown_function( "translate_shutdown_error_to_exception");

That's it, that's ALL we need to capture all meaningful errors as exceptions. And, here's an obligatory example of what I mean!

try {
    $value = 10/0;
} catch (ErrorException $e) {
    // ...
}

Now, when do you use it? THAT's the tricky question.

I decided to do some benchmarks to see, not only what is possible, but what is ADVISABLE. I ran three tests, in two environments, with 20,000 iterations (times shown in seconds). The three tests were the following.

The two environments were: WITHOUT the above error handlers active (in case they might interact negatively with other code), and then WITH them. And the 20,000 iterations are obviously how many times I looped over the test code. (1,000 was enough to see these trends but I figured 20-times that was sufficient for a blog post. Don't tell me I never did anything for you!)

Here are the code tests

// Error-free, no-mod
for($i=0; $i<MAX; $i++) {
  $a = 10/10;
}

// Error-full, no mod
for($i=0; $i<MAX; $i++) {
  $a = 10/0;
}

// avoiding the problem code via a conditional, no mod
for($i=0; $i<MAX; $i++) {
  if (0 > 0) {
    $a = 10/0;
  }
}

// Error-free, with mod (same code as first example)
for($i=0; $i<MAX; $i++) {
  $a = 10/10;
}

// Error-full, with exceptions (THIS IS THE INTERESTING ONE!)
for($i=0; $i<MAX; $i++) {
  try {
    $a = 10/0;
  } catch (ErrorException $e) {}
}

// avoiding the problem code via a conditional, with mod
for($i=0; $i<MAX; $i++) {
  if (0 > 0) {
    $a = 10/0;
  }
}

Okay, got that out of the way. Now the results. :)

Error-free, no-mod: 0.0132961273193
Error-full, no-mod: 21.1321649551
Conditional, no-mod: 0.0135869979858

Error-free, with-mod: 0.0151519775391
Error-full, with-mod: 1.63587808609
Conditional, with-mod: 0.013237953186

Let's discuss the mundane first. Error-free code runs about the same with-or-without the modifications. Since these don't even interact that's not surprising. Conditional code also has the same performance -- it seems correct code is better than incorrect code! Wow, the things you learn...

Anyway, on to the buggy code. Error-full (that's the one where we were kicking out 'Divide By Zero' errors) actually took a long time (21 seconds). This is REALLY expensive because PHP feels compelled to output all of the errors to the browser (or in my case the command prompt). I actually used ob_start()/ob_end_clean() to avoid that (it was 44 seconds without). Interestingly we also see that the code using exceptions only took 1.6 seconds. From this we can determine that exceptions are cheaper than errors.

Exceptions seem to be about 20x faster than errors (in my situation, at least) but using proper avoidance is another 100x faster.

Yes, for all you nay-sayers, we *should* still avoid using exceptions when we can otherwise detect a faulty state. But if the choice is between exception or error, choose exception -- not only that, but having a consistent system for handling bugs is AMAZING!

Related Posts