Hi,
since many people seem to struggle a lot with error reporting and exception handling, and since the same nonsense „solutions“ still get copied and pasted around, this is an attempt of clearing things up a bit.
Premises
What error reporting is supposed to do:- distinguish between end-users and developers
- provide developers with detailed error reports
- provide end-users with a short, generic error message and a 5xx status code
What error reporting is not supposed to do- leak critical information (file paths, queries, your database credentials, ...)
- irritate users with technical bug reports
- stay silent
Common mistakes
Displaying internal errors generated by PHP
This is probably the most common mistake. Outputting PHP errors makes sense during development, but on the actual website, it’s very dangerous for you and irritating for your users. The PHP error messages are meant for developers, not application users. If they pop up on your website, they help attackers with detailed information while scaring legitimate users.
Since this often happens due to a misconfiguration, make sure to properly configure PHP as explained below.
Displaying internal errors from subsystems
Another common mistake is taking the error messages from, for example, MySQL and dumping them on the screen with a
die() or an
echo. Check this pattern, which is an all-time favorite of many bullsh*t „tutorials“ and gets copied and pasted around since PHP was invented:
don’t use
PHP Code:
mysql_query(...) or die(mysql_error());
There’s also an object-oriented variant:
don’t use
PHP Code:
try {
$db->query(...);
} catch (PDOException $e) {
echo 'Query failed: ' . $e->getMessage();
}
What’s wrong with this is that it again displays an internal error message not meant for end-users. The message will contain critical information like the exact query or even your database credentials. But it’s actually even worse than a misconfiguration, because this mistake cannot be fixed globally. A
die() is a
die(), you cannot turn it off. You have to actually go through your whole code and delete those parts manually.
So never use
echo or
die() to display error messages from databases or other systems.
Proper error reporting
Configuring PHP
Start off by opening the php.ini. Note that you cannot do the error configuration within the script, because PHP might fail before the script is even executed.
The four most important directives are:
- error_reporting: which errors should be displayed or logged
- display_errors: whether or not internal error messages should be displayed on the website
- display_startup_errors: whether or not errors within the PHP system itself should be displayed
- log_errors: whether or not errors should be written to a log; the log can be specified with error_log
During development, you’ll probably want to see all error messages in the browser and not log them. Do this by setting
error_reporting to
-1,
display_errors and
display_startup_errors to
On and
log_errors to
Off. You can also exclude specific errors using the error constants. Avoid using
E_ALL. Despite its name, it doesn’t cover
E_STRICT errors prior to PHP 5.4.
Use during development
Code:
error_reporting = -1
display_errors = On
display_startup_errors = On
log_errors = Off
On the actual website, PHP must not output any internal error messages, so set
display_errors and
display_startup_errors to
Off. If you want to log the errors, set
error_reporting to the appropriate level, turn
log_errors on and set the
error_log if needed.
Use for production
Code:
error_reporting = -1 ; if your code is sloppy, this will quickly fill the error log, so use an appropriate level
display_errors = Off
display_startup_errors = Off
log_errors = On
Setting up a custom error page
If you’re using nginx with a current PHP version (at least 5.2.4) over FastCGI, this is easy: enable
fastcgi_intercept_errors and define an
error_page for 500 errors.
PHP >= 5.2.4 will automatically generate a 500 code under the following conditions: a fatal error has occured,
display_errors is set to
Off and no output has been sent yet. All you have to do is catch the 500 code and have nginx handle it.
Use with nginx + PHP >= 5.2.4
Code:
fastcgi_intercept_errors on;
error_page 500 /error_pages/5xx.html;
location /error_pages {
internal; # the error pages should not be publicly accessible
}
If you’re using Apache, you need to do some handiwork. Since Apache doesn’t intercept 500 errors issued by PHP, you’ll get a blank page by default. Any custom error page must be sent by PHP itself. However, you
cannot do this in the actual script, because several errors like
E_PARSE prevent the script from ever being executed (as already mentioned above).
The workaround for this is to create a separate script with a shutdown handler and prepend it to every script using
auto_prepend_file in the php.ini:
Code:
auto_prepend_file = 'path/to/fatal_error_handler.php'
A shutdown handler is executed after the actual script has been run or aborted, so it can be used to catch even severe errors like erroneous syntax. Prepending the helper script makes sure that the shutdown handler is registered before the actual script might crash everything.
Use with Apache + PHP >= 5.4
PHP Code:
<?php
register_shutdown_function('handle_fatal_errors');
function handle_fatal_errors() {
// display error message if the response code is 500 (either due to a fatal error or a custom header() call) and display_errors is turned off and nothing has been sent yet
if ( http_response_code() == 500 && !ini_get('display_errors') && !headers_sent() ) {
// flush any buffered content (assuming this is garbage generated before the error occured)
ob_clean();
// send a 500 status code and an error message
echo 'Sorry, an error has occured. The webmaster has been notified and will fix the issue as soon as possible. Please try again later.';
}
}
Use with Apache + PHP < 5.4
PHP Code:
<?php
register_shutdown_function('handle_fatal_errors');
function handle_fatal_errors() {
$fatal_errors =
E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR;
$last_error = error_get_last();
// display error message if there was a fatal error and display_errors is turned off and nothing has been sent yet
if ( $last_error && ($last_error['type'] & $fatal_errors) && !ini_get('display_errors') && !headers_sent() ) {
// flush any buffered content (assuming this is garbage generated before the error occured)
ob_clean();
// send a 500 status code and an error message (see http://stackoverflow.com/questions/3258634/php-how-to-send-http-response-code)
header('X-PHP-Response-Code: 500', true, 500);
echo 'Sorry, an error has occured. The webmaster has been notified and will fix the issue as soon as possible. Please try again later.';
}
}
Using custom errors or exceptions
In case you want to generate a custom error within the script (like when a query fails), always use the
trigger_error() function or an exception (see below). This will adhere to the error procedure and not just dump the message on the screen no matter what.
PHP Code:
$query = mysql_query(...);
if (!$query)
trigger_error('Query failed: ' . mysql_error(), E_USER_ERROR);
Note that the old MySQL extension is hopelessly outdated and has already been replaced 10 years ago. If possible, switch to one of the new extensions. They no longer require you to manually check the status of each query.
Exceptions
If you’re dealing with modern, object-oriented PHP code, you have to consider yet another error mechanism: exceptions. An exception is a special object indicating a certain kind of error.
Since exceptions are pretty „new“ in the PHP world, they don’t seem to be very well understood. A common misconception is that somehow every piece of code that might throw an exception must be wrapped in a try-catch block. This is
not the case.
Check this piece of code from the PHP manual, which is yet another example of bad ideas being mindlessly copied and pasted through the whole internet:
don’t use
PHP Code:
try {
$db = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
This is dangerous, error-prone and completely useless for many reasons:
- It will simply dump the error message on the screen, completely ignoring the PHP configuration. The error will leak critical information about your database and your queries (see „Common mistakes“).
- It does what an uncaught exception would do, anyway – except that it cuts off important information like the file name, line, stack trace etc. and turns a smart exception into a dumb die().
- The code can hardly be reused in a bigger system, because it could stop the whole application at any time. There’s no chance to catch the exception on a higher level. Wasn’t reusability the whole point of object-oriented programming?
- Doing this procedure for every query clutters up the whole application with useless, repetitious code
So this is obviously a bad idea. It’s probably just meant to demonstrate how exceptions can be fetched, but many people actually copy and paste the literal code.
Exceptions are designed to be
smart. They do not have to be fetched right away like a return value. Instead, they can „bubble up“ and be caught later – or not at all. An uncaught exception will generate a fatal error and trigger the standard error procedure, which should work fine for most of your exceptions.
This allows for very sophisticated error handling – but only if used properly:
- Only catch an exception if you actually want to handle it in a specific way. In case of a database failure, for example, you might want to retry the connection, switch to an alternative connection etc. Otherwise, just let the exception bubble up and catch it later or not at all.
- Don't repeat yourself. When you have multiple exceptions that should all be handled in the same way, catch them on a higher level.
- Catch specific exceptions, not the generic Exception class. This class includes all exceptions, so you might unintentionally „swallow“ other important errors.