PHP Error Handling: Never Say die() Again

Published:
Introduction
This article is intended for those who are new to PHP error handling.  It addresses one of the most common problems that plague beginning PHP developers: effective error visualization.

PHP error handling is well-documented in the online man pages, but the documentation often eludes beginners, who are trying to learn PHP by copying examples they found on the internet.  Copying the code without understanding the code is an antipractice, and there are so many bad PHP examples out there, it can sometimes be difficult to find good ones!  This article will help you get the most, quickly and easily, out of PHP's basic error handlers.  You can get excellent error information, informative and well-targeted, by just following a few simple examples.  So stop copying those obsolete internet examples, and instead make a few "teaching examples" for your own library.  By the time you've finished this article, you will have some great insights into how to diagnose the most common errors quickly and find good solutions.

What's Covered Here
PHP has unlimited opportunities for errors, but in practice only a few things are needed to get good diagnostics.  You need to be able to see the errors in PHP and you need to be able to see the errors in the PHP extensions, like MySQLi.  These require some PHP settings, and a little bit of code to call out the errors in the extensions.  This article will show how to get PHP to report the errors and how to get the extensions to report their errors, too.

PHP Won't Help You if You Won't Help Yourself!
PHP has a long history of trying to be "easy to use."  One of the early strategies for ease of use was, some might say, a bit misguided.  That early strategy was to produce as little error information as possible.  Many error conditions produced nothing more than a Notice-level message.  But the Notice-level messages were suppressed in the standard PHP installation.

This had the unfortunate effect of letting PHP scripts fail silently at run time, or fail silently and intermittently when the script relied on an undefined variable, a condition that could occur because of a typographical error.  Today we know better, and many of those early silent failures are accompanied by warnings.  But not all of them are warned.  You have to tell PHP that you want to know about the errors.  You do that with error_reporting() and a few associated settings.

Settings for Development and Deployment
For most PHP applications you want these settings in the development environment.  We are telling PHP to report all the errors, write them into the error_log file, and display them in the browser viewport.
<?php
                      error_reporting(E_ALL);
                      ini_set('log_errors', TRUE);
                      ini_set('error_log', 'error_log');
                      ini_set('display_errors', TRUE);

Open in new window


For most PHP applications you want these settings in the deployed (production) environment
<?php
                      error_reporting(E_ALL);
                      ini_set('log_errors', TRUE);
                      ini_set('error_log', 'error_log');
                      ini_set('display_errors', FALSE);

Open in new window


See the difference?  The deployed environment does not display the errors, but it still logs them.

What happens when there is an error in the deployed application?  You will probably get the "white screen of death,"
depending on the exact location of the error.  But no matter what appears in the browser viewport, you will still get the error_log information, so you can investigate the error after the fact.

Error Suppression: A Symptom of Bad Error Handling
Perhaps you have seen something like this in a PHP code sample.
<?php
                      @mysql_query( ... );

Open in new window


To the trained eye, that is scary.  See the @ ("at-sign") prepended to the function name?  It's the error suppression operator.  And it's like dog poop.  It is out of place wherever you find it.  Here is what one PHP veteran says on the PHP web site. 


Error suppression should be avoided if possible as it doesn't just suppress the error that you are trying to stop, but will also suppress errors that you didn't predict would ever occur. This will make debugging a nightmare.
In my experience, there is almost never any justification for using this.  So just don't!  Instead, write a bit more code to test for the error conditions you expect.  And avoid those error conditions.

If you ever find the @ operator in a sample of PHP code, expect to find a prominent explanation in the associated comments.  And if you don't find that explanation, you might want to skip over the example.  It's likely that the example came from a novice programmer.  There may be other errors lurking in the code, too.

Obscure Error Messages: Another Symptom of Bad Error Handling
Poor error handling is a code smell that can be fairly obvious to the trained eye.  It is a code smell because it can leave you frustrated as you see your script fail in a way that tells you nothing about the cause or remedy.  One common symptom of inadequate error handling may be the white screen of death.  Another common symptom is a meaningless message like "Query failed" or "Cannot connect."  These are problematic conditions because they don't tell you anything about what went wrong or where it went wrong.  Debugging a script without any meaningful message is like poking about in a dark room - you might hit something, but it's hard to know what's in there.

On the other hand, good error handling is like turning on the lights.

The first and most common mistake is using die().  PHP die() is functionally equivalent to exit(), but die() is seen more frequently in PHP code examples.  It terminates the script, and optionally produces a message of the user's choice.  Here is an example that produces a nearly meaningless error message and terminates the script.

<?php
                      $link = mysqli_connect($db_host, $db_user, $db_word, $db_name) or die('Cannot connect to database');

Open in new window


Here is another example that shows the same poor strategy.
<?php
                      $result = mysqli_query($link, 'SELECT * FROM myTable') or die('Query failed');

Open in new window


These examples illustrate bad error handling because
1. They terminate the script, even when it may not be necessary;
2. They give no indication of where the failure occurred;
3. They give no indication of why the failure occurred.

A Quick Upgrade to Better Error Handling
What's a better solution than die()?  It's trigger_error().

When trigger_error() is used with appropriate diagnostic functions, you can get much better information without even trying
1. You don't have to terminate the script unless you want to;
2. You automatically get the path information and line numbers showing where the error occurred;
3. You automatically get an indication of why the failure occurred.

Now that you know about trigger_error() what should you do?  Scan your code library for every instance of a call to exit() or die() and make yourself a project to change these to trigger_error() in each and every case that trigger_error() can possibly be used!  Some good examples follow.

Digging Deeper - Getting Meaningful Help from PHP Extensions
Almost all PHP functions return some value, and smart programmers know to test these return values.  However many of the outdated examples on the internet do not test the return values, but instead blindly use the results, often with potentially disastrous consequences.  A particularly common example seems to arise when using the MySQL database extensions. (Note: the MySQL extension has been removed from PHP, but the terrible code examples still litter the internet!)

When you upgrade from MySQL to MySQLi, you've got a great opportunity to upgrade your error handling.  Here's that earlier connect example with improved error handling:

<?php
                      $link = mysqli_connect($db_host, $db_user, $db_word, $db_name);
                      if (!$link)
                      {
                          $err
                          = 'MySQLi Error: '
                          . mysqli_connect_errno()
                          . ' '
                          . mysqli_connect_error()
                          ;
                          trigger_error($err, E_USER_ERROR);
                      }

Open in new window


When a connect failure occurs you will get a message something like this:

Fatal error: MySQLi Error: 1045 Access denied for user ''@'localhost' (using password: NO) in /path/to/script.php on line ...
Armed with that information, you can make a Google search for MySQL Error: 1045 Access denied for user, and you will links to a variety of helpful resources!

If you wanted to get a little fancy, you could even set up the Google search in advance!
<?php
                      $link = mysqli_connect($db_host, $db_user, $db_word, $db_name);
                      if (!$link)
                      {
                          $msg
                          = 'MySQLI Error: '
                          . mysqli_connect_errno()
                          . ' '
                          . mysqli_connect_error()
                          ;
                          $err
                          = '<a target="_blank" href="https://www.google.com/#q='
                          . urlencode($msg)
                          . '">'
                          . htmlentities($msg)
                          . '</a>'
                          ;
                          trigger_error($err, E_USER_ERROR);
                      }

Open in new window


You will see the same message as before, but it will come primed with a clickable link to the helpful resources.

How about the queries?  You can use the same strategy, but you need to call different error functions.
<?php
                      $sql = 'SELECT Error';
                      $result = mysqli_query($link, $sql);
                      if (!$result)
                      {
                          $msg
                          = 'MySQLI Error: '
                          . mysqli_errno($link)
                          . ' '
                          . mysqli_error($link)
                          ;
                          $err
                          = '<a target="_blank" href="https://www.google.com/#q='
                          . urlencode($msg)
                          . '">'
                          . htmlentities($msg)
                          . '</a>'
                          . " SQL: $sql"
                          ;
                          trigger_error($err, E_USER_ERROR);
                      }

Open in new window


A Bonus: Trigger_Error() is Extensible!
All by itself, trigger_error() is a great improvement over die(), but that's not all.  Trigger_error() can be used in conjunction with the built-in error handler, or with a user defined function that has been set as the new error handler.  It's beyond the scope of this article, but you might want to read about set_error_handler() if you want more advanced functionality.  There is also ErrorException, but I have not personally found a good use for it yet.

Finding and Reading the Error Logs
Whether your application is in development or already deployed, the error_log file is a valuable resource.  In my PHP installation, the error_log file is only created if an error has occurred.  It may be appended to, if there are successive errors.  Going through a lot of server directories to look for error_log can be a laborious task, but we can automate it.  I use a script like this one to walk through the web directory tree.  It finds and displays the error_log files and gives me a push-button link to remove the error_log.
<?php // /find_error_log.php
                      /**
                       * Put this script in the web root or other top-level directory
                       *
                       * Traverse this directory and all directories of the web tree
                       * Show and optionally delete the error log files
                       *
                       * http://php.net/manual/en/class.recursivedirectoryiterator.php#85805
                       */
                      ob_start();
                      error_reporting( E_ALL );
                      ini_set( 'display_errors', TRUE );
                      ini_set( 'log_errors',     TRUE );
                      
                      
                      // START IN THE CURRENT DIRECTORY
                      $path = realpath(getcwd());
                      $plen = strlen($path);
                      
                      // THE ERROR LOG FILE NAME - REVERSED BECAUSE IT IS AT THE END OF THE PATH STRING
                      $error_log = ini_get('error_log');
                      $error_log = basename($error_log);
                      $error_log = strrev($error_log);
                      
                      // IF THERE IS A POST-METHOD REQUEST TO DELETE THIS ERROR LOG
                      if (!empty($_POST['error_log']))
                      {
                          // MAKE SURE WE ONLY UNLINK THE ERROR LOG FILE
                          $test = strrev($_POST['error_log']);
                          if (strpos($test, $error_log) === 0)
                          {
                              @unlink($path . $_POST['error_log']);
                              echo '<h3>' . $_POST['error_log'] . ' Discarded</h3>';
                          }
                      }
                      
                      
                      // COLLECT THE DIRECTORY INFORMATION OBJECTS
                      $objs = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST);
                      
                      
                      // ITERATE OVER THE OBJECTS
                      foreach($objs as $name => $obj)
                      {
                          // PROCESS THE ERROR LOG ONLY
                          $test = strrev($name);
                          if (strpos($test, $error_log) === 0)
                          {
                              // CREATE A DELETE BUTTON FOR THIS ERROR LOG
                              $name = substr($name, $plen);
                              $form = <<<EOD
                      <form method="post" style="margin:0; padding:0; display:inline;!important">
                      <b>$name</b>
                      <input type="submit" value="Discard?" />
                      <input type="hidden" name="error_log" value="$name" />
                      </form>
                      EOD;
                              echo $form;
                      
                              // SHOW THE CONTENTS OF THIS ERROR LOG
                              echo '<pre>';
                              print_r(file_get_contents($path . $name));
                              echo PHP_EOL . '********** EOF **********';
                              echo '</pre>' . PHP_EOL;
                          }
                      }
                      
                      
                      // IF THERE ARE NO ERROR LOG(S)
                      $out = ob_get_contents();
                      if (empty($out)) echo '<h3>Good News! No error_log found.</h3>';
                      
                      
                      // SHOW THE GIT BRANCH
                      $root = '.git/HEAD';
                      $text = @file_get_contents($root);
                      if ($text)
                      {
                          $text = explode(DIRECTORY_SEPARATOR, $text);
                          $text = array_slice($text, 2);
                          $name = implode(DIRECTORY_SEPARATOR, $text);
                          echo PHP_EOL . "On Git branch: $name" . PHP_EOL;
                      }
                      else
                      {
                          echo PHP_EOL . "On Git branch: UNKNOWN" . PHP_EOL;
                      }
                      
                      echo '<a href="' . $_SERVER['REQUEST_URI'] . '">Run Again</a>?' . PHP_EOL;
                      
                      // SCRIPT TERMINATION WILL FLUSH THE OUTPUT BUFFER TO THE CLIENT BROWSER

Open in new window


Checking Your PHP Settings
In case you're wondering about what PHP settings are in play, you can find everything by running this little script, shown here in its entirety.  It produces a lot of output, but with a browser search you can find the "good stuff."  You might try running this and checking the output for "error" to see what your settings are.
<?php phpinfo();

Open in new window


Conclusion
An enormous volume of PHP code has been propagated with die() when a more reasonable code set would use trigger_error() instead.  This article has shown how to upgrade your code, and why you never want to use die() again.

If you find that this article has been helpful, please click the “thumb’s up” button below. Doing so lets the E-E community know what is valuable for E-E members and provides direction for future articles. Thanks!
3
6,644 Views

Comments (1)

Most Valuable Expert 2011
Author of the Year 2014

Author

Commented:
Thanks, Martin.

Some time ago when I published articles, there was a cascade of approvals - something like "It's OK" then "It's good" then "It's really good" and each of these approvals gave some more points as well as comments about how the article can be made better.  Does E-E still do that?

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.