Manage PHP errors and exceptions in your project
Benjamin Grandfond8 min read
During application development, developers have to handle errors in the execution flow. PHP, among many other languages, allows you to do so but since I recently stumbled upon a really bad way to do it I thought that reminding the basics would not hurt.
Errors or Exceptions
PHP makes a distinction between “errors” and “exceptions”. What’s the difference? Since version 4, PHP provides errors to tell that something went wrong. It is possible to trigger errors and register an error handler but they are unrecoverable. Later, with the release of PHP 5, exceptions were introduced to be used in an object oriented way. Exceptions are catchable, unlike errors, meaning that you can catch them and try to recover or continue with the execution of the program.
When does “error” occur?
Despite the introduction of exceptions, errors are still here and continue to appear (I don’t mean that exceptions should replace errors)… Try something like this:
<?php
$foo =[bar];
echo $foo:
Notice that quotes are missing in the array key: I should have written ['bar']
. This produces the following error:
PHP Notice: Array to string conversion[…]
Common errors include:
- parsing errors: missing parenthesis, braces, semi-column…
- type conversion errors
- memory allocation errors
Generally errors occur at the language level like when the syntax is wrong, some actions over variables are invalid.
Error reporting level
Depending on the gravity of the error, your code can continue to run until the end or will stop. You can configure error reporting in PHP to ignore minor errors but I would recommend you to report as many errors as possible while developing. To configure the error reporting level you will use constants and bitwise operators which are not easy to apprehend. For example, to remove the notice from the previous piece of code you would do the following:
<?php
error_reporting(E_ALL & ~E_NOTICE);
// or simply
error_reporting(~E_NOTICE);
$foo = ["bar"];
echo $foo;
If you want to know what your current error reporting value is, use the error_reporting() function without any arguments.
Trigger errors manually
Errors can also be manually raised. For example, if you want to deprecate a function and warn the developer that this function will be removed in the next release, then you would do something like this:
<?php
/**
* Returns the addition of to integer
*
* @deprecated This function will be removed in the release 0.2, you should use the add function instead
* @param integer $a
* @param integer $b
* @return integer
*/
function calculate($a,$b) {
trigger_error("This function will be removed in the release 0.2, you should use the add function instead", E_USER_DEPRECATED);
return add($a, $b);
}
/**
* @param integer $a
* @param integer $b
* @return integer
*/
function add($a, $b)
{
return $a $b;
}
As you can see the function add($a, $b)
has been added, for more clarity I guess. So the calculate($a, $b)
triggers an error E_DEPRECATED
, which, with the default PHP configuration, does not stop the execution of the script, and calls the add()
function.
When the script calls the calculate function, the deprecation message is displayed:
PHP Deprecated: This function will be removed in the release 0.2, you should use the add function instead
Handling errors
By default errors will be output right after it has been triggered or after the execution of the script. However, you can catch it and do things by defining and registering an error handler.
set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) {
echo "\n Have a nice day\n";
exit(1);
});
echo calculate(2, 3);
exit(0);
In this example we reuse the previous calculate function that triggers a E_USER_DEPRECATED error. We set the closure as the error handler that will be called whenever an error occurs.
As you have already guessed this closure will print “Have a nice day” and exit with the code 1, meaning that the script ended with a problem.
benjamin@comp : ~ $ php error-test.php
Have a nice day
Introduction to exception
As mentioned before, exceptions have been introduced with PHP 5 to be used with the new way to program in PHP: object oriented (of course exceptions can also be used with a procedural way).
Exceptions are easy to use, you only have to instantiate a new Exception object with an explicit message, (optionally a code and a parent exception), and throw it:
throw new Exception(sprintf('Cannot find the file "%s".', $file));
Unlike errors, you can catch a thrown exception and decide that your code can continue even if it failed somehow.
// File exception.php
class FileReader
{
/**
* @params string The file full path
* @return string
* @throws Exception
*/
public function read($file)
{
$realFile = realpath($file);
if (!file_exists($realFile)) {
throw new Exception(sprintf('The file "%s" does not exist', $file));
}
return file_get_contents($realFile);
}
}
This is a class that is able to read the content of a file. If the file does not exist it raises an exception telling so. Let’s try to use it:
// File exception.php
$reader = new FileReader();
echo $reader->read('/foo/bar');
exit(0):
benjamin@comp : ~$ php exception.php
PHP Fatal error: Uncaught exception 'Exception' with message 'The file "/Users/benjamin/exception.php/foo" does not exist' in /Users/benjamin/exception.php:64
Stack trace:
>#0 /Users/benjamin/exception.php(72): FileReader->read('/Users/benjamin...')
#1 {main} thrown in /Users/benjamin/exception.php on line 64
PHP tells you that an exception has been raised but not caught and results in a fatal error that stops the execution of the script. To fix this you simply need to surround the call of the method by a try/catch statement:
// File exception.php
$reader = new FileReader();
try {
echo $reader->read('/foo/bar');
} catch (Exception $e) {
echo $e->getMessage();
exit(1);
}
exit(0);
Then when executing the exception.php script the exception message is echoed instead of the ugly message produced before.
Handling exceptions
Sometimes, some exceptions could have not been caught by the library you use for your project causing a fatal error as seen previously. In this case you should set an exception handler to avoid problems when an exception has not been caught.
// File exception.php
set_exception_handler(function (Exception $e) {
echo $e->getMessage();
exit(1);
});
$reader = new FileReader();
echo $reader->read('/foo/bar');
exit(0);
This will produce the same output than previously: it will echo the exception message and exit with code 1.
Convert errors to exceptions
As I said before, errors can’t be caught whereas exceptions can. But you can handle errors and convert them to exceptions thanks to the ErrorException class. To do so you need to register an error handler which converts errors into ErrorException. Lets do this with our previous calculation code:
set_error_handler(function ($errno, $errstr, $errfile, $errline ,array $errcontex) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
try {
$result = calculate(2, 3);
} catch (ErrorException $e) {
echo sprintf("An error has been caught\n %s", $e->getMessage());
$result = 0;
}
echo $result;
Thanks to the closure used as an handler, errors are now converted to an ErrorException that can be caught the way we saw previously and allows your code to behave accordingly to the exception. Your code will no longer stops because of an ugly error :)
Multi catching and re-throwing exceptions
As there are different exception types you might want to differentiate the caught one to behave accordingly. You can chain the catch statement this way to tell your script to do something distinct:
try {
// do something that could raise an Exception or an ErrorException
} catch (ErrorException $e) {
// do something when an ErrorException occurred
} catch (Exception $e) {
// do something when an Exception occurred
}
Note that the Exception is caught last because ErrorException extends the Exception class.
Finally
Since PHP 5.5, you can specify a finally block after the catch one. The code inside is always executed even if an exception has been thrown or not. Be careful if you use a return statement inside a try/catch/finally block, don’t forget that the last return statement executed is the finally one.
Semantic exceptions
PHP Core provides two exception classes: Exception and ErrorException. But if you want you can create your own extending the Exception
class. Doing so you can check the type of the thrown exception to do something special as we saw previously. Here is an example where two exceptions are thrown. The first when the file to read does not exist. The other one when it is not readable:
// File exception.php
class FileReader
{
/**
* @params string The file full path
* @return string
* @throws Exception
*/
public function read($file)
{
$realFile = realpath($file);
if (!file_exists($realFile)) {
throw new FileNotFoundException(sprintf('The file "%s" does not exist', $file));
}
if (!$content = file_get_contents($realFile)) {
throw new FileNotReadableException(sprintf('The file "%s" is not readable', $file));
}
return $content;
}
}
class FileNotFoundException extends Exception { }
class FileNotReadableException extends Exception { }
Now we know that when a FileNotFoundException is caught we can create the missing file:
// File exception.php
$reader = new FileReader();
try {
$file = '/foo/bar.txt';
echo $reader->read($file);
} catch (FileNotFoundException $e) {
$filesystem->touch($file);
} catch (Exception $e) {
echo $e->getMessage();
exit(1);
}
exit(0);
When you create your exception to should give them explicite name, like you would do with business classes. Using such exceptions allows you to understand the problem seeing the type without looking at the message. You should always try to make your code speaking to you, explaining what really happened to ease the debugging. Creating special exceptions will help you doing so, but you still have to provide explicit messages…
Before creating your own exception, have a look a those provided by the SPL library:
- BadFunctionCallException
- BadMethodCallException
- DomainException
- InvalidArgumentException
- LengthException
- LogicException
- OutOfBoundsException
- OutOfRangeException
- OverflowException
- RangeException
- RuntimeException
- UnderflowException
- UnexpectedValueException
Now you know the difference between errors and exceptions. You are able to create error handlers and exception handlers to be sure that your program will never stop with a fatal error. You saw how to catch exceptions. You learned how to create your own exception. So I hope that from now on you will use exceptions instead of returning true or false or returning an array with the status and a message when an exception would be better.