PHP Error Handling
- Handling Errors
- Handling External Errors
- Exceptions
- When to Use Exceptions
- Further Reading
Errors are a fact of life. Mr. Murphy has an entire collection of laws detailing the prevalence and inescapability of errors. In programming, errors come in two basic flavors:
External errorsThese are errors in which the code takes an unanticipated path due to a part of the program not acting as anticipated. For example, a database connection failing to be established when the code requires it to be established successfully is an external error.
Code logic errorsThese errors, commonly referred to as bugs, are errors in which the code design is fundamentally flawed due to either faulty logic ("it just doesn't work that way") or something as simple as a typo.
These two categories of errors differ significantly in several ways:
External errors will always occur, regardless of how "bug free" code is. They are not bugs in and of themselves because they are external to the program.
External errors that aren't accounted for in the code logic can be bugs. For example, blindly assuming that a database connection will always succeed is a bug because the application will almost certainly not respond correctly in that case.
Code logic errors are much more difficult to track down than external errors because by definition their location is not known. You can implement data consistency checks to expose them, however.
PHP has built-in support for error handling, as well as a built-in severity system that allows you to see only errors that are serious enough to concern you. PHP has three severity levels of errors:
E_NOTICE
E_WARNING
E_ERROR
E_NOTICE errors are minor, nonfatal errors designed to help you identify possible bugs in your code. In general, an E_NOTICE error is something that works but may not do what you intended. An example might be using a variable in a non-assignment expression before it has been assigned to, as in this case:
<?php $variable++; ?>
This example will increment $variable to 1 (because variables are instantiated as 0/false/empty string), but it will generate an E_NOTICE error. Instead you should use this:
<?php $variable = 0; $variable++; ?>
This check is designed to prevent errors due to typos in variable names. For example, this code block will work fine:
<? $variable = 0; $variabel++; ?>
However, $variable will not be incremented, and $variabel will be. E_NOTICE warnings help catch this sort of error; they are similar to running a Perl program with use warnings and use strict or compiling a C program with Wall.
In PHP, E_NOTICE errors are turned off by default because they can produce rather large and repetitive logs. In my applications, I prefer to turn on E_NOTICE warnings in development to assist in code cleanup and then disable them on production machines.
E_WARNING errors are nonfatal runtime errors. They do not halt or change the control flow of the script, but they indicate that something bad happened. Many external errors generate E_WARNING errors. An example is getting an error on a call to fopen() to mysql_connect().
E_ERROR errors are unrecoverable errors that halt the execution of the running script. Examples include attempting to instantiate a non-existent class and failing a type hint in a function. (Ironically, passing the incorrect number of arguments to a function is only an E_WARNING error.)
PHP supplies the trigger_error() function, which allows a user to generate his or her own errors inside a script. There are three types of errors that can be triggered by the user, and they have identical semantics to the errors just discussed:
E_USER_NOTICE
E_USER_WARNING
E_USER_ERROR
You can trigger these errors as follows:
while(!feof($fp)) { $line = fgets($fp); if(!parse_line($line)) { trigger_error("Incomprehensible data encountered", E_USER_NOTICE); } }
If no error level is specified, E_USER_NOTICE is used.
In addition to these errors, there are five other categories that are encountered somewhat less frequently:
E_PARSEThe script has a syntactic error and could not be parsed. This is a fatal error.
E_COMPILE_ERRORA fatal error occurred in the engine while compiling the script.
E_COMPILE_WARNINGA nonfatal error occurred in the engine while parsing the script.
E_CORE_ERRORA fatal runtime error occurred in the engine.
E_CORE_WARNINGA nonfatal runtime error occurred in the engine.
In addition, PHP uses the E_ALL error category for all error reporting levels.
You can control the level of errors that are percolated up to your script by using the php.ini setting error_reporting. error_reporting is a bit-field test set that uses defined constants, such as the following for all errors:
error_reporting = E_ALL
error_reporting uses the following for all errors except for E_NOTICE, which can be set by XOR'ing E_ALL and E_NOTICE:
error_reporting = E_ALL ~ E_NOTICE
Similarly, error_reporting uses the following for only fatal errors (bitwise OR of the two error types):
error_reporting = E_ERROR | E_USER_ERROR
Note that removing E_ERROR from the error_reporting level does not allow you to ignore fatal errors; it only prevents an error handler from being called for it.
Handling Errors
Now that you've seen what sort of errors PHP will generate, you need to develop a plan for dealing with them when they happen. PHP provides four choices for handling errors that fall within the error_reporting threshold:
Display them.
Log them.
Ignore them.
Act on them.
None of these options supersedes the others in importance or functionality; each has an important place in a robust error-handling system. Displaying errors is extremely beneficial in a development environment, and logging them is usually more appropriate in a production environment. Some errors can be safely ignored, and others demand reaction. The exact mix of error-handling techniques you employ depends on your personal needs.
Displaying Errors
When you opt to display errors, an error is sent to the standard output stream, which in the case of a Web page means that it is sent to the browser. You toggle this setting on and off via this php.ini setting:
display_errors = On
display errors is very helpful for development because it enables you to get instant feedback on what went wrong with a script without having to tail a logfile or do anything but simply visit the Web page you are building.
What's good for a developer to see, however, is often bad for an end user to see. Displaying PHP errors to an end user is usually undesirable for three reasons:
It looks ugly.
It conveys a sense that the site is buggy.
It can disclose details of the script internals that a user might be able to use for nefarious purposes.
The third point cannot be emphasized enough. If you are looking to have security holes in your code found and exploited, there is no faster way than to run in production with display_errors on. I once saw a single incident where a bad INI file got pushed out for a couple errors on a particularly high-traffic site. As soon as it was noticed, the corrected file was copied out to the Web servers, and we all figured the damage was mainly to our pride. A year and a half later, we tracked down and caught a cracker who had been maliciously defacing other members' pages. In return for our not trying to prosecute him, he agreed to disclose all the vulnerabilities he had found. In addition to the standard bag of JavaScript exploits (it was a site that allowed for a lot of user-developed content), there were a couple particularly clever application hacks that he had developed from perusing the code that had appeared on the Web for mere hours the year before.
We were lucky in that case: The main exploits he had were on unvalidated user input and nondefaulted variables (this was in the days before register_global). All our database connection information was held in libraries and not on the pages. Many a site has been seriously violated due to a chain of security holes like these:
Leaving display_errors on.
Putting database connection details (mysql_connect()) in the pages.
Allowing nonlocal connections to MySQL.
These three mistakes together put your database at the mercy of anyone who sees an error page on your site. You would (hopefully) be shocked at how often this occurs.
I like to leave display_errors on during development, but I never turn it on in production.
Production Display of Errors
How to notify users of errors is often a political issue. All the large clients I have worked for have had strict rules regarding what to do when a user incurs an error. Business rules have ranged from display of a customized or themed error page to complex logic regarding display of some sort of cached version of the content they were looking for. From a business perspective, this makes complete sense: Your Web presence is your link to your customers, and any bugs in it can color their perceptions of your whole business.
Regardless of the exact content that needs to be returned to a user in case of an unexpected error, the last thing I usually want to show them is a mess of debugging information. Depending on the amount of information in your error messages, that could be a considerable disclosure of information.
One of the most common techniques is to return a 500 error code from the page and set a custom error handler to take the user to a custom error page. A 500 error code in HTTP signifies an internal server error. To return one from PHP, you can send this:
header("HTTP/1.0 500 Internal Server Error");
Then in your Apache configuration you can set this:
ErrorDocument 500 /custom-error.php
This will cause any page returning a status code of 500 to be redirected (internallymeaning transparently to the user) to /custom-error.php.
In the section "Installing a Top-Level Exception Handler," later in this chapter, you will see an alternative, exception-based method for handling this.
Logging Errors
PHP internally supports both logging to a file and logging via syslog via two settings in the php.ini file. This setting sets errors to be logged:
log_errors = On
And these two settings set logging to go to a file or to syslog, respectively:
error_log = /path/to/filename error_log = syslog
Logging provides an auditable trace of any errors that transpire on your site. When diagnosing a problem, I often place debugging lines around the area in question.
In addition to the errors logged from system errors or via trigger_error(), you can manually generate an error log message with this:
error_log("This is a user defined error");
Alternatively, you can send an email message or manually specify the file. See the PHP manual for details. error_log logs the passed message, regardless of the error_reporting level that is set; error_log and error_reporting are two completely different entries to the error logging facilities.
If you have only a single server, you should log directly to a file. syslog logging is quite slow, and if any amount of logging is generated on every script execution (which is probably a bad idea in any case), the logging overhead can be quite noticeable.
If you are running multiple servers, though, syslog's centralized logging abilities provide a convenient way to consolidate logs in real-time from multiple machines in a single location for analysis and archival. You should avoid excessive logging if you plan on using syslog.
Ignoring Errors
PHP allows you to selectively suppress error reporting when you think it might occur with the @ syntax. Thus, if you want to open a file that may not exist and suppress any errors that arise, you can use this:
$fp = @fopen($file, $mode);
Because (as we will discuss in just a minute) PHP's error facilities do not provide any flow control capabilities, you might want to simply suppress errors that you know will occur but don't care about.
Consider a function that gets the contents of a file that might not exist:
$content = file_get_content($sometimes_valid);
If the file does not exist, you get an E_WARNING error. If you know that this is an expected possible outcome, you should suppress this warning; because it was expected, it's not really an error. You do this by using the @ operator, which suppresses warnings on individual calls:
$content = @file_get_content($sometimes_valid);
In addition, if you set the php.ini setting track_errors = On, the last error message encountered will be stored in $php_errormsg. This is true regardless of whether you have used the @ syntax for error suppression.
Acting On Errors
PHP allows for the setting of custom error handlers via the set_error_handler() function. To set a custom error handler, you define a function like this:
<?php require "DB/Mysql.inc"; function user_error_handler($severity, $msg, $filename, $linenum) { $dbh = new DB_Mysql_Prod; $query = "INSERT INTO errorlog (severity, message, filename, linenum, time) VALUES(?,?,?,?, NOW())"; $sth = $dbh->prepare($query); switch($severity) { case E_USER_NOTICE: $sth->execute('NOTICE', $msg, $filename, $linenum); break; case E_USER_WARNING: $sth->execute('WARNING', $msg, $filename, $linenum); break; case E_USER_ERROR: $sth->execute('FATAL', $msg, $filename, $linenum); echo "FATAL error $msg at $filename:$linenum<br>"; break; default: echo "Unknown error at $filename:$linenum<br>"; break; } } ?>
You set a function with this:
set_error_handler("user_error_handler");
Now when an error is detected, instead of being displayed or printed to the error log, it will be inserted into a database table of errors and, if it is a fatal error, a message will be printed to the screen. Keep in mind that error handlers provide no flow control. In the case of a nonfatal error, when processing is complete, the script is resumed at the point where the error occurred; in the case of a fatal error, the script exits after the handler is done.
Mailing Oneself
It might seem like a good idea to set up a custom error handler that uses the mail() function to send an email to a developer or a systems administrator whenever an error occurs. In general, this is a very bad idea.
Errors have a way of clumping up together. It would be great if you could guarantee that the error would only be triggered at most once per hour (or any specified time period), but what happens more often is that when an unexpected error occurs due to a coding bug, many requests are affected by it. This means that your nifty mailing error_handler() function might send 20,000 mails to your account before you are able to get in and turn it off. Not a good thing.
If you need this sort of reactive functionality in your error-handling system, I recommend writing a script that parses your error logs and applies intelligent limiting to the number of mails it sends.