PHP error reporting

  1. TL;DR
  2. The Rules
  3. Bad, bad examples
  4. Just leave them alone!
  5. What if some function refuses to raise an error?
  6. Converting errors to exceptions
  7. Showing a nice page for a user
  8. The universal handling function
  9. Handling fatal errors
  10. The complete error handling example code
  11. Comments (8)

Due to various historical reasons, there are thousands of bad practices in the area of error handling and reporting. As a result, the error reporting is often inflexible, hard to maintain, not user-friendly or even harmful.

All the confusion is coming from the fact that every site has two kinds of customers: a programmer and a user, who require totally different treatment in regard of error messages:

The exact moment you'll have these things sorted out, you'll know what to do with error messages in PHP. Below you will find a set of basic principles that will help you to satisfy such contradicting demands, as well as their complete practical implementation

TL;DR

As a rule, do not add error reporting operators to your PHP code (especially ones that output the error message right to the screen). Instead, make PHP generate errors by itself. And then, configure PHP to direct these messages appropriately: on a dev server they have to be shown on-screen; whereas on a live server they should be logged while only a generalized error page without any particular details should be shown to a site visitor.

To do so, configure your PHP as follows:

Note that these values are better to be set in php.ini or in the the server's configuration:

Only as a last resort set them right in the script using ini_set() command:

error_reporting(E_ALL);
ini_set('display_errors', 1);

but it will fail for some errors (parse errors for example).

And then create an error handler for a live site that is showing an excuse page in case of PHP error.

To put it short: write your scripts without any dedicated error reporting code. Instead, just configure PHP to report errors by itself. Only by the time your code goes live you will need some error handling code but that could be a short code snippet that would handle all errors the uniform and centralized way.

The Rules

To configure error reporting the right way, one should always follow these two cornerstone rules:

Instead, the error message should be logged for the future reference, whereas just a generic error page should be shown to a site visitor.

Bad, bad examples

And such a separation - between a live and a dev server - is the very source of confusion. As a matter of fact, many newbie PHP users are writing their code as though they will always be the only users of their site, bluntly directing error messages right on the screen. Don't you believe me? How many times did you see such a code:

// BAD example! Do not use!
$result = mysqli_query($conn, $query) or die(mysqli_error($conn));

or

// BAD example! Do not use!
try {
    $stmt = $pdo->query($sql);
} catch (PDOException $e) {
    die($e->getMessage());
}

?

Many php tutorials, including, sadly, the PHP manual, are using this terrible approach. Given the rules above, you can tell why it's wrong: it is intended for the development only, but absolutely unsuitable for a live site.

Also, error messages must not be suppressed. This includes

All these bad practices prevent PHP from telling a programmer what the problem is . And we already learned that error messages are extremely important.

So, how to arrange the error handling to fulfill such contradicting guidelines: showing errors in the full force for a developer yet hide them completely from a site visitor?

Just leave them alone!

The answer is surprisingly simple: just leave error messages alone

Although it sounds quite alien to many PHP users (who cannot imagine a single database interaction without several lines of diligently written error handling code), yet this is the very truth: by leaving the error message alone, one will make its handling flexible and easily configurable, making it possible to switch between modes with just a single PHP configuration option!

Just think of it: there is a php.ini directive called display_errors. By setting it to 1 we will switch our site into development mode, as PHP will start showing every error occurred automatically, without any effort from our side! And by setting it to 0, we will mute error messages completely, barring a hacker from the critical information and making a user less confused. Still, we need to show a page with excuses, but that's a little different story which we will discuss later.

The same goes for exceptions: an uncaught Exception makes a regular PHP error, so it makes no sense to write a special code only to show it: PHP can show you errors already! So just leave it alone and it will behave exactly as any other error, according to the site-wide rules.

What if some function refuses to raise an error?

There are some PHP modules which, by default, silently fail instead of raising an error. Well, most likely they have a secret configuration option that will make them behave as good boys - raise errors by themselves. If some PHP function doesn't raise an error but apparently fails, then look around for the option to turn error reporting on for its module. For example, PDO could (and should) be configured to throw exceptions, as well as mysqli.

But sometimes there is no way at all to make a function report its error by itself. A good example is the late mysql_query() function or json_decode()*: both will silently return false in case of error. In this situation you should look around once more and search the error message provider dedicated to this function. In our case it will be mysql_error() and json_last_error_msg() respectively.

But again, these functions' output should never ever be fed to the notorious die() operator!

Instead, it should be transferred into a PHP error. The simplest way to do so is to use the trigger_error() function:

$result = mysql_query($query) or trigger_error(mysql_error());

in case you have to deal with legacy code that is ultimately using this outdated extension that has been completely removed from the language in 2015, at least the error reporting should be done this way.

Even better would be to throw an exception. The simplest way is just to test the function's result and to add a throw operator (note that you cannot use it neatly with or operator, the condition should be explicit):

if ($error = json_last_error_msg())
{
    throw new \Exception("JSON decode error: $error");
}

But that's just a quick and dirty solution. The best way would be to create a dedicated Exception class for such an error. It's not a big deal, basically it's just a single line:

class JsonDecodeException extends ErrorException {}

yet after defining the dedicated class we can create a more talkative version of json_decode():

function jsonDecode($json, $assoc = false)
{
    $ret = json_decode($json, $assoc);
    if ($error = json_last_error())
    {
        throw new JsonDecodeException(json_last_error_msg(), $error);
    }
    return $ret;
}

Now it will start spitting the error telling us the reason, just like expected:

Fatal error: Uncaught exception 'JsonDecodeException' with message 'Syntax error' in /home/me/test.php:9
Stack trace:
#0 /home/me/test.php(14): jsonDecode('foobar')
#1 {main}
  thrown in /home/me/test.php on line 9

This message will be sent either to the screen or the error log, telling us that there was an error with decoding JSON, caused by the wrong JSON syntax.

*Update: Since 7.3.0 it is possible to tell json_decode to throw an exception.

Converting errors to exceptions

Historically, there was only errors in PHP. Then in PHP 5 exceptions were added to the language and since then PHP is moving slowly to replace errors with ErrorExceptions. As this process is not finished yet, we have to deal with two possible kinds of errors - errors and ErrorExceptions. It is not so convenient. Besides, exceptions are more versatile than errors - they could be caught, and they contain a stack trace by default. So consider turning all errors into exceptions, as it will allow the uniform error handling. The code to do so is ridiculously simple:

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

all we have to do is just to call a set_error_handler() function and tell it to throw an exception.

Add these three lines to your PHP scritps and you'll be able to catch PHP errors! For example, the following code

try {
    $a = 1/0;
} catch (Throwable $e) {
    echo "Come on, you cant't divide by zero!";
}

will tell you not to divide by zero instead of throwing an error. Quite convenient. And again, it will make the error handling uniform.

Showing a nice page for a user

All right, it's OK with a programmer, they are notified of the every error occurred, either from a log file or just by watching the screen. But what about a site user? With proper error reporting set, they will just face a blank page, which is apparently not the way to go. We need to show them some explanations and ask to try later. There are several ways to do that. The simplest way is to configure your web-server to show such a page in case of error. For example, to configure Nginx to show a custom error page in case of 500 error, just create such a custom error page, and then add this line to the server configuration:

error_page 500 502 503 504 /500.html

Voila! In case of error this page will be shown instead of a blank screen, making your users happy! Do not forget to create the actual 500.html page and put it in the site root.

However, it is reported that Apache's mod_php has issues with its ErrorDocument directive and a 500 error generated by PHP. To make the solution robust and have more control on the error handling it's better to handle error on the PHP side, but still do it the centralized way.

For this, PHP offers two functions, set_error_handler() and set_exception_handler(). They both register a function that will be called in case an error or an uncaught exception appears.

Given we already used the error handler function to convert errors to exception, now we need only an exception handler function.

set_exception_handler(function ($e)
{
   // here we can put some code to notify a user about an error.
});

The universal handling function

But having such a function for just a user's convenience will make the programmer unhappy, as the latter wants to see the actual error message. So let's create a universal function that could satisfy both. In order to do so we need some parameter to tell a dev environment from a production one. There are many possible solutions, in my case I would just use the display_errors php.ini configuration parameter. As it must be set to 0 on the production server we could use it to tell that we are in the production environment. But of course you could use your own markers, for example using a constant is very common solution.

set_exception_handler(function ($e)
{
    error_log($e);
    http_response_code(500);
    if (ini_get('display_errors')) {
        echo $e;
    } else {
        echo "<h1>500 Internal Server Error</h1>
              An internal server error has been occurred.<br>
              Please try again later.";
    }
});

Note that this is a very basic function. Many additions could be made to it, in order to improve the output for both a site user and a programmer. Yet, this is already a robust solution that will make your site comply with the basic error reporting rules.

Handling fatal errors

One last thing to make our error handling completely uniform.

Fatal errors are not caught by the standard error handler. To handle them the same way as other errors, we will need another function, register_shutdown_function which will tell PHP to call a function every time the PHP execution is terminated, both of natural causes or in case of error. So we will need to tell the latter from the former, for which the error_get_last() will be used, also providing the error information:

register_shutdown_function(function ()
{
    $error = error_get_last();
    if ($error !== null) {
        // here we can put some code to notify a user about an error.
    }
});

Unfortunately, this function is called at such a stage when no intelligent handling is possible, for example, an exception thrown inside this function wont be caught using try-catch operator. All we can do is some basic error handling.

The complete error handling example code

Now let's try to gather together everything we learned, and create a basic error handling solution.

First, we need to configure basic error handling directives in the PHP/web-server configuration as discussed here. Then let's create a file called error_handler.php with the following code inside, and include it in your bootstrap file:

<?php

error_reporting(E_ALL);

function myExceptionHandler ($e)
{
    error_log($e);
    http_response_code(500);
    if (filter_var(ini_get('display_errors'),FILTER_VALIDATE_BOOLEAN)) {
        echo $e;
    } else {
        echo "<h1>500 Internal Server Error</h1>
              An internal server error has been occurred.<br>
              Please try again later.";
    }
    exit;
}

set_exception_handler('myExceptionHandler');

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

register_shutdown_function(function ()
{
    $error = error_get_last();
    if ($error !== null) {
        $e = new ErrorException(
            $error['message'], 0, $error['type'], $error['file'], $error['line']
        );
        myExceptionHandler($e);
    }
});

Here, we define a common error handling function MyExceptionHandler() which encapsulates all the error handling logic. Then, we set up the exception handler that calls our function when an uncaught exception appears.
Also, all PHP errors are converted into exceptions as it was discussed above, and thus end up processed by the same handling code.
Finally, the same handling function is also called for the fatal errors.

That fancy code to get the display_errors is due to the fact that the value could be not only 1 or 0 but also "yes", "no", "true", etc. And FILTER_VALIDATE_BOOLEAN would handle them all correctly.

Of course, this basic solution could be greatly improved and extended. But the idea here is to provide a concise, easy to grasp educational example, that could be nevertheless used in the production code as well. Do not hesitate to extend this basic approach to satisfy your needs.

If you want some ideas on the advanced error handling, take a look at Laravel's error handling code


Related articles: