PHP error reporting
- TL;DR
- The Rules
- Bad, bad examples
- Just leave them alone!
- What if some function refuses to raise an error?
- Converting errors to exceptions
- Showing a nice page for a user
- The universal handling function
- Handling fatal errors
- The complete error handling example code
- 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:
- a programmer who needs to see every error possible with every minute detail provided
- a regular site user who must see not a single error message, but rather a generic excuse page instead
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:
-
On a development server
error_reporting
should be set toE_ALL
value;display_errors
should be set to 1log_errors
could be set to 1
- On a production server
error_reporting
should be set toE_ALL
value;display_errors
should be set to 0log_errors
should be set to 1
Note that these values are better to be set in php.ini
or in the the server's configuration:
- for the Apache's mod_php it should be set in
httpd.conf
or.htaccess
- for PHP CGI it should be set in the local
php.ini
file - for php-fpm it should be set in the corresponding
.conf
file in/etc/php-fpm.d
.
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:
- On a developer's PC the error message should be shown in the full detail, along with other useful information, to ease the development process.
- On a live site, not a single word from the error message should be shown to a site user. Because:
- for a casual user it is pretty cryptic and confusing.
- it it essentially useless for the site programmer/admin as they likely won't be browsing the site at the moment and will have no idea the error has been occurred.
- the information provided in the error message is extremely useful for a potential hacker, as it provides them with the feedback for their actions and may leak some sensitive information about your site.
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
- the use of
@
operator - setting
error_reporting
to zero (this is a most common one as people often confuseerror_reporting
(which actually stands for "error reporting level") withdisplay_errors
which they're actually looking for) - an empty
catch {}
block -
an if operator that checks the function call's result but does nothing in case of error (or outputs some meaningless stuff like "Error!", "Query failed", etc.). Like:
// BAD examples! Do not use! if ($stmt = $mysqli->prepare($query)) { // ...proceed with the query } // or if ($stmt = $mysqli->query($query)) { // ...proceed with the query } else { echo "FAILED"; }
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:
- Relative and absolute paths, in the file system and on the web server.
- Do you really need to check for both isset() and empty() at the same time?
- What's wrong with popular articles telling you that foo is faster than bar?
- MVC in simpler terms or the structure of a modern web-application
- How to get a single player's rank based on the score
- Articles
- Do you abuse the null coalescing operator (and isset/empty as well)?
- Operator precedence or how does 'or die()' work.
- Why should I use prepared statements if escaping is safe?
- Numerical strings comparison
- Do generators really reduce the memory usage?
- How to make Stack Overflow a nice place
- Iterating over array getting look-ahead items along
- How to debug small PHP programs
Add a comment
Please refrain from sending spam or advertising of any sort.
Messages with hyperlinks will be pending for moderator's review.
Markdown is now supported:
>
before and an empty line after for a quote