PHP Error Handling

NYPHP - PHundamentals

PROBLEM:

Every well-constructed PHP application should have error handling. While there is no definitive method for handling errors since it varies depending on application needs and a developer's style, nonetheless, there are some "best practices" that should be implemented in all PHP applications.

WHAT IS AN ERROR?

The answer may seem straightforward but actually requires a bit of discussion. An "error" is an expected or unexpected event that occurs when your PHP code is running. An "expected" error can be a database query that returns no result or an html form that is missing values for designated required fields. An "unexpected" error is one that assumes a particular application state which, for some as yet unknown reason, does not exist. The most obvious unexpected error is a database that is not running or a missing file that one of your scripts expects to find.

ERROR HANDLING ENVIRONMENT

The recommended "best practice" has three parts:

  1. The developer should be alerted to all errors, notices, warnings, etc. both during development and when the PHP application goes into production.
  2. The developer should be able to choose how to be notified of these problems.
  3. No errors, notices, warnings, etc. should ever be displayed to the user. Even in development, errors dumped to the browser can be bad because they can become hidden in the HTML. In production, you should display a generic page that says "System down for maintenance" or some other generic message. At no time do you want to tip off a hacker as to what went wrong, nor do you want to display the details of what went wrong. That information is for you alone. Although some developers feel that errors should be displayed during development and even in a production environment - modified, of course, so as not to tip off a hacker - we will assume here that errors will never be displayed.


We can set these three parameters in one of two ways.

  1. If you have control over the php.ini file then set the following parameters:
    (See Note 1)

    error_reporting = E_ALL
    log_errors = On
    display_errors = Off

  2. If you don't have control over php.ini, then at the beginning of your script you can set the same parameters using the following:

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


When "log_errors" is set to "On", errors will go into one of two places:

  1. The Apache error log file
    If you control your own server, you can view the Apache error logs using the following command:
        tail -f /path/to/your/log/error_log
    The " -f " setting will show the errors as they are appended to the file. If the error has already occurred, you can use the following command to view the last 100 lines of the log. tail -100 error_log | more The number "100" can be changed to any number of lines you may want to view and piping the output to the more command will show a screen's worth of data at a time.

  2. In a shared hosting environment, to a specified file within your designated directory. In this environment, you may need to download the error log file in order to view it.

In either case, the file that PHP will write its errors to can be changed in either the php.ini file in the "Error Handling and Logging" section, specifically, error_log = filename or, if you do not have control over php.ini, you can set the file in your code by using the following:

ini_set('error_log', '/full/path/to/your/file/my-errors.log');


EXPECTED ERRORS

This type of error can be the the result of something a website visitor has done such as providing invalid form input (the visitor omitted some values or entered letters in a field that is expecting numbers) or it can be a database query that returns no records. For example, you may have a form that requires that particular fields be completed before submitting or particular fields may require a particular type of input such as only numbers and dashes in a field that captures phone numbers. Therefore, your PHP code should ALWAYS check for acceptable values.

Illustrated below is a simple example that illustrates the concept. Click here to see it in action.

<?php
/**************************************************************
 Check values entered by user.
 The code shown here is meant solely to illustrate the concept
 in the main article. There are many different coding methods
 that one can use to validate form input.
***************************************************************/

//Initialize array to an empty array
$errMsg = array();

/*
 Check to see if the form has been posted. If true, then
 check values.
*/
if (isset($_POST['submit'])){
 if (
trim($_POST['Name']) === ''){
    
$errMsg[] = 'Please enter your name';
  }
  
//We want to make sure that the phone number is in the format we want
  
if (!preg_match("/^[0-9]{3}[-]{1,1}[0-9]{4}$/", $_POST['Phone'])){
    
$errMsg[] = 'Phone number should be entered as digits in the following format: ###-####';
  }
  
/*
  If an error has occurred, the array "$errMsg" will have a count greater than zero
  so display our error message to the user
  */
  
if (count($errMsg)!== 0){
    foreach (
$errMsg as $key => $value){
      
$tmp .= "<li>$value</li>";
    }   
    echo
"An error has occurred:<ul>$tmp</ul>";
  } else {
    echo
"Congratulations! You successfully entered your Name and Phone Number!";
  }
}
//Note below that we are echoing back to the user any values already entered and we are
//making those values browser-safe with the "htmlentities" function
//(See Note 2)
?>


<form name="myForm" method="POST" action="<?php echo $_SERVER['PHP_SELF']?>">
Name: <input name="Name" type="text" value="<?php echo htmlentities($_POST['Name'])?>">
<
br />
Phone: <input name="Phone" type="text" value="<?php echo htmlentities($_POST['Phone'])?>">
<
input type="submit" name="submit" value="Submit">
</
form>


UNEXPECTED ERRORS

Unexpected errors are errors that are not part of the normal operation of your PHP application such as a database that suddenly stops running. When an unexpected error occurs, you want to:

  1. know the state of the application
  2. know when it happened
  3. know that it happened
  4. have a record of the above three items
  5. prevent the user from knowing any of the details of what happened (See Note 3)

Below are general guidelines for implementing our recommended best practices for notifying the developer of unexpected errors.

  1. In the top level include file for your application, set the error handler for your application to your own error handler by using

    set_error_handler
    ('myErrHandler');


    You could simply place this statement at the beginning of all of your PHP scripts but using an "include" file makes it easier to maintain your code. See require_once and related functions and the PHundamentals article Site Structure: Where to Locate Includes?.
  2. When an error occurs, trigger the error handler, e.g.,

    if(!mysql_connect("myDatabase","myUser","myPassword")){   
    trigger_error('Can not connect to database',E_USER_ERROR);
    }


So what should your error handler look like? We've provided a sample error handler though the exact error handler you use is solely up to you. However, the sample code below fulfills the five basic requirements of an error handler as noted above.

  1. The error handler lets you know the "state" of the application (handled by trigger_dump())
  2. The error handler lets you know when it happened (handled by date())
  3. The error handler lets you know that it happened (handled by mail())
  4. The error handler records all three of the items above (handled by error_log())
  5. You need to prevent the user from knowing any of the details of what happened, except that it happened (handled by header)

<?php
function myErrHandler($code, $message, $errFile, $errLine) {
  
//Set message/log info
  
$subject = "$message: MAJOR PROBLEM at " . APP_NAME . ': ' . date("F j, Y, g:i a");
  
$body = "errFile: $errFile\r\n"
            
. "errLine: $errLine\r\n"
            
. trigger_dump($GLOBALS);

  
  /*
    An email will be sent to the site administrator.
    Its subject line will have the date and time it occurred while
    the body will contain the state of all of the global variables. This information
    is obtained through the function trigger_dump.
  */
  
mail(ADMIN_EMAIL_ADDR,$subject,$body);


  
//The same subject line and body of the email will get written to the error log.
  
error_log("$subject\r\n $body");


  /*
    We don't want users to know the true nature of the problem so
    we just redirect them to a generic error page that has been created.
    The generic page should have a simple message, such as "System down
    for maintenance." The key idea is not to let any potentially malicious
    user learn about the actual problem that had occurred.
  */
  
header ("Location: http://{$_SERVER['HTTP_HOST']}/". GENERIC_ERR_PAGE );
  exit;
}

  /*
    The function below is called by the mail
    and error_log calls above.
  */

function trigger_dump( $mixed,$type = 0 ) {
  /*
    $mixed will handle whatever you may decide to pass to it.
    $type will determine what this function should do with the
    information obtained by var_dump
  */
  
switch( (int) $type ) {
    case
0:
      
/*
      Grab the contents of the output buffer
      when you want to send the information
      back to the calling function
      */
      
ob_start();
      
var_dump($mixed);
      
//If you are using PHP ver. 4.3 use the
      //code below:
      
return ob_get_clean();
      
//If you are using an earlier version
      //of PHP, then use the code below:
      
$ob_contents = ob_get_contents();
      
ob_end_clean();
      return
$ob_contents;
    case
1:
      
/*
       When you want to display the information to the browser
      */
      
print '<pre>';
      
var_dump($mixed);
      print
'</pre>';
      break;
    case
2:
      
//When you want your error handler to deal with the information
      
ob_start();
      
var_dump($mixed);
      
//If you are using PHP ver. 4.3 use the
      //code below:
      
trigger_error(ob_get_clean());
      break;
      
//If you are using an earlier version
      //of PHP, then use the code below:
      
$ob_contents = ob_get_contents();
      
ob_end_clean();
      
trigger_error($ob_contents);
      break;
    }
}
?>


The constants used in the sample code are part of a sample include file. Click here to view the file.
Case 2 will always send the output of var_dump to your own error handler, via the trigger_dump function. For additional information see var_dump and also trigger_error.

NOTES:

1 - For additional information on php.ini settings, see the latest version of php.ini-recommended. See also the PHundamentals article PHP Initialization. A list of configuration settings that can be changed via ini_set can be found here.

2 - For additional information on the use of htmlentities see the PHundamentals article "Storing Data Submitted From a Form and Displaying Data from a Database". While you can use JavaScript to trap form-related user errors, you should NEVER solely rely on it. Users can intentionally turn off JavaScript and thereby bypass your error checking. For a discussion of other form-related issues, see the PHundamentals article Spoofed Form Submissions.

3 - Whether to show a generic "System down for maintenance" message or not is, of course, a judgement call on the part of the developer. For example, one could simply redirect the user to the home page which would be a visual indicator (to the user) that something had gone awry but it does not tip off the user as to "what" went awry. In the case of "expected" errors, such as a search function that uses URL parameters, you might want to implement a set of "default" search parameters. For example, your site might have a URL like the following: http://mydomain.com/search.php?location=USA. If a user should alter the search parameter, e.g., location=123, your code could handle this by automatically substituting the default search parameters. Alternately, you could display a message such as "No records were found. Please try again."


Contributors to this note include the following:
John F. Andrews
Mark Armendariz
David Mintz
Mitch Pirtle
Chris Shiflett
Hans Zaunere
the PHundamentals Team: Jeff Siegel, Michael Southwell