- A Survey of PHP Error Handling
- Magento’s Mini Error Framework
- Laravel’s Custom Error Handler
It’s a daily event across the PHP development world. Somewhere, someone is thinking, and possibly typing
I updated the code and it stopped with no error
This sort of question used to trigger my inner programmer-rage-monster because there’s almost always an error message. While there’s a few legitimate cases where a programming language’s error messaging system may short circuit, it’s far more common that a poorly configured system is to blame.
The error messages are there, you just need to know how to find them.
I say this used to trigger my inner programmer-rage-monster, because it turns out that the myriad different ways of configuring error reporting in PHP means you can never be sure if error reporting and logging is behaving the way you think it should.
This article attempts to, if not reign in the PHP error reporting white screen monster, then to provide a road map for successfully debugging errors on your PHP system.
What’s an Error
First off, let’s define what we mean by a PHP error. Let’s say you’re building an accounting system and discover it’s incorrectly doing a revenue split. This is not a PHP error. This is a logic error in your application. PHP is doing exactly what you told it to, it’s just that the problem was more complicated than you thought.
A PHP error is when you write a piece of code PHP can’t understand. This might be something that’s explicitly disallowed by the system (calling a member function on a non-object, for example), or may be something that the PHP team has decided is a bad idea (using a variable before it’s been defined). Either way, a PHP error is something wrong with your code and not something wrong with your application.
Some errors make PHP stop in its tracks. For example, leaving off a semi-colon at the end of a line
echo $foo
echo $foo;
will trigger a parsing error. If PHP can’t parse the file, it has no idea what to do, so it stops. It’s the same with the dreaded “call to a member function on a non-object” error
$foo = 1;
echo $foo->render();
The above code just tried to call a method (render
) on an integer — which makes no sense in PHP. Again, PHP halts execution.
There’s a second class of errors php can recover from. These are often the result of syntax that used to be valid PHP, but in retrospect, syntax the PHP team decided was a bad idea. This is an example of PHP’s backwards compatible philosophy. The old bad behavior is still allowed, but PHP will raise an error, and then continue with the program. This lets old “bad” code continue to do what it did, but gives more disciplined developers path out of the muck.
For example, the following code results in a PHP error
#File: example.php
<?php
echo $foo;
echo "Done","\n";
since $foo
is undefined.
$ php example.php
Notice: Undefined variable: foo in /path/to/example.php on line 8
Done
Even though echo "Done";
happens after this error, PHP still outputs the text Done
, because an Undefined variable
error is a recoverable error.
Regardless of whether PHP can recover from an error, it generates an error message. Consider the above error
Notice: Undefined variable: foo in /path/to/example.php on line 8
The error message here is Undefined variable: foo in /path/to/example.php on line 3. This error message gives us a short description of the error (or what PHP has identified as the error), the file the error occurred in (example.php
), and the line number where the error occurred (8
).
While these error messages are useful, sometimes they’re misleading. For example, the following code
#File: example.php
<?php
if($foo == 'bar')
echo $test;
}
produces the error
Parse error: syntax error, unexpected '}' in /path/to/example.php on line 3
At first blush this error makes no sense — why wouldn’t an if
clause expect a closing }
— plus we have one?
The reason for this message (which some of you may have spotted), is the PHP parser saw this code more like this
if($foo == 'bar') echo $test;
}
That is, a single clause if statement, followed by a rouge }
. To a human, the error was the missing opening bracket ({
), but a code parser can’t see this.
While these error messages are useful, you’ll still need to use your brain and examine/analyze why a particular piece of code is causing a problem.
Error Logging
Now that we know what an error is, you’re probably wondering where PHP puts these error messages. There’s multiple places a PHP error message may end up, depending on your system’s configuration.
First is the error logging. If the php.ini
value log_errors
is set to true, PHP will write the error message to a log file.
log_errors = On
This value is also configurable at runtime via ini_set
.
ini_set('log_errors',1);
so changing your php.ini
file may not be enough depending on the framework/application code.
The log_errors
ini flips logging on and off. The error_log
ini setting controls which file PHP will log its error messages to.
error_log = /tmp/php_errors.log
You may also set this value at runtime with ini_set
ini_set('error_log','/tmp/log2.log');
So again, you application or framework may be changing this file’s location after you’ve set a value in php.ini
Many stock installations of PHP do not set an error_log
value.
If your error_log
value is blank, PHP will pass the error message onto the web server. This means your PHP error message will end up in your web server’s error log. In apache, this is configured (in httpd.conf
or one of its include files) with
ErrorLog "/var/log/apache2/error_log"
If you’re having trouble finding your system’s apache log, try doing something like the following at the start of your program or right before the point in your code that’s causing an error.
ini_set('log_errors','1');
ini_set('error_log','/tmp/my-custom-php-error-log.log');
These two ini
settings will both ensure error logging is on, as well as set a custom error log file in your system’s tmp
folder.
Finally, make sure your web server has the proper permissions to write to the error_log
file you’ve specified. If the web server user lacks these permissions, PHP will fall back to logging to the web server’s error log.
Error Printing
In addition to the error log, messages are also printed to the browser, or “standard output” (STDOUT
) if running in command line (CLI) mode.
The display of a PHP error is controlled by the display_errors
ini setting.
display_errors = 1
Like the other ini
settings we’ve discussed, you may also set this at runtime.
ini_set('display_errors', '1');
If we add this to out magic “show me the errors” ini recipe, we’ll end up with something that looks like this
ini_set('log_errors','1');
ini_set('error_log','/tmp/my-custom-php-error-log.log');
ini_set('display_errors', '1');
If you use PHP from the command line, you may have noticed it often prints errors twice.
PHP Notice: Undefined variable: foo in /path/to/example.php on line 5
Notice: Undefined variable: foo in /path/to/example.php on line 5
That’s because the command line version of PHP is both logging and displaying the errors. If the error_log
value is not set, a logged error is sent to the shell’s STDERR
stream, and then the “displayed” error (as per normal) is sent to the shell’s STDOUT
stream.
This also draws attention to a slight difference between a logged error and a displayed error. A displayed error starts with the string PHP Notice
, but a logged error starts with the string Notice
. The leading PHP
is included in the logged error to help system administrators tell which system is generating this particular error, while a displayed error drops this leading PHP
, assuming the developer will know what language they’re using.
So, you’ve got everything configured correctly, but your errors still aren’t showing up. Nothing in the error log, and nothing in the browser, but we still have a halting, or “white screen of death” application. What gives?
The likely culprit is the PHP “error reporting” feature.
Error Reporting
In addition to configuring where errors are reported, PHP (being PHP) lets you configure what errors are actually considered errors in your system. This is done via the error_reporting
function or error_reporting
ini
settings.
For example, lets consider our invalid code from before
error_reporting(E_ALL | E_STRICT); //trust us on this line for now
echo $foo;
echo "Done","\n";
If you you run the above code, you’ll end up with an error like this
Notice: Undefined variable: foo in ...
However, consider this almost identical code
error_reporting(E_PARSE); //trust us on this line for now
echo $foo;
echo "Done","\n";
Run this, and PHP will not display and will not log the error. We’ve effectively told PHP to ignore all PHP Notice
errors (and a slew of others). Using a variable before it’s defined is, effectively, no longer an error on our system.
Of course, if other code expects that variable to be defined you’ll end up with a weirdly behaving program — which is why PHP considers this worth raising as an error by default.
Error Levels
The error_reporting
function works by assigning every possible PHP error an “error level”. Current versions of PHP ship with 16 different error levels.
E_ERROR
E_WARNING
E_PARSE
E_NOTICE
E_CORE_ERROR
E_CORE_WARNING
E_COMPILE_ERROR
E_COMPILE_WARNING
E_USER_ERROR
E_USER_WARNING
E_USER_NOTICE
E_STRICT
E_RECOVERABLE_ERROR
E_DEPRECATED
E_USER_DEPRECATED
E_ALL
An E_ERROR
is the most serious type of error — a fatal PHP error. Next is E_WARNING
, etc., all the way up to E_USER_DEPRECATED
. E_ALL
is a special case that turns on all the errors, and we’ll discuss it fully later on. By putting every error into a specific error level, PHP lets you (the user) decide which errors you want to see. Only want to see fatal errors? Set the level to E_ERROR
error_reporting(E_ERROR);
In addition to setting the error reporting level at runtime with the error_reporting
function, you may also set the level with the error_reporting
ini value in a php.ini file
; PHP's `ini` parser will understand the error constants
error_reporting = E_PARSE;
Or via the ini_set
function
ini_set('error_reporting', E_PARSE);
Error Reporting Gotachas
There are, of course, some gotchas. The one that’s probably on your mind is the error reporting level we used in our example script
error_reporting(E_ALL | E_STRICT);
What’s that E_ALL | E_STRICT
all about? There’s two things going on here. The first is PHP’s bitwise operator constant system. E_ALL
, E_STRICT
, etc. are PHP constants that resolve to particular numbers. These numbers are the actual error levels. Like many PHP constant systems, the error_reporting
mechanism allows you to combine certain constants using bitwise operators. We’ll describe this system in the Bitwise Constant System below — it’s an “AP” level topic that you don’t need to fully comprehend to move forward — just know that we’re saying “show us all all the errors” when we say error_reporting(E_ALL | E_STRICT);
More important though is, why is this necessary? Shouldn’t E_ALL
be enough? In versions of PHP 5.4 and above, it is. Unfortunately, in previous versions of PHP, E_STRICT
errors were not included in the special E_ALL
error reporting level. This was a classic PHP internals compromise. The E_STRICT
level was introduces to
“have PHP suggest changes to your code which will ensure the best interoperability and forward compatibility of your code.”
This was, in part, an effort to rid PHP of certain weird behaviors that had been preserved for backwards compatibility reasons. Consider something like this
class A
{
public function test()
{
echo "Hello PHP — weep for me.","\n";
}
}
echo A::test();
A modern PHP programmer would look at this and cringe. We’re using a static calling pattern (A::test()
) to call a non-static function. This should clearly be an error. However, in PHP 4, which had no concept of static member functions, the above syntax was valid, (and introduced to allow users the ability to use methods on a class that didn’t rely on object state. PHP 4 was weird — tread lightly).
PHP 5.3 made this an E_STRICT
error. In practice this meant when PHP 5.3 was released to the world many applications which had set their error_reporting
level to E_ALL
suddenly found themselves inundated with all sorts of E_STRICT
warnings. To make life easier for these folks, E_STRICT
errors were removed from E_ALL
. This solved some specific problems, at the cost of further complicating an already complicated system.
Long story short: If you want PHP 5.3 to report all your errors, use E_ALL | E_STRICT
. If you’re interested in the nitty gritty details of PHP’s bitwise constant system, see the Bitwise Constant System section below
Before moving on, let’s revisit out SHOW ALL THE ERRORS code snippet and add the error_reporting
call.
error_reporting(E_ALL | E_STRICT);
ini_set('log_errors','1');
ini_set('error_log','/tmp/my-custom-php-error-log.log');
ini_set('display_errors', '1');
Custom Error Handling
So, that’s a pretty long list of things to keep track of for PHP error reporting. Surely that’s everything, right? Right?
Of course not! While the error level system allows a fine grained control over which errors show up, it’s not enough for some users (especially if they don’t understand the bitwise constant system. For these users we have the set error handler function.
The set_error_handler
function allows an end user to define their own error handling function. It accepts a PHP callback/callable pseudo type, which indicates which function, anonymous function, static method, or object method, should be called when a PHP error happens.
Consider a modified version of our basic script above
#File: example.php
function ourMagicErrorHandler($error_level, $error_string, $error_file, $error_line, $error_context)
{
echo "An error happened but we're not going to say which one","\n";
return true;
}
set_error_handler('ourMagicErrorHandler');
error_reporting(E_ALL | E_STRICT); //trust us on this line for now
echo $foo;
echo "Done","\n";
Here we’ve set the error handler as our custom function ourMagicErrorHandler
. If we run this code (which still contains the undefined variable error), we’ll see the following.
$ php example.php
An error happened but we're not going to say which one
Done
PHP’s normal error handling has been completely suppressed in favor of our own. If we want normal PHP error handling to resume after our custom error handler call, we should return false
.
echo "An error happened but we're not going to say which one","\n";
return false;
Running our script with the above would product output like this
$ php example.php
An error happened but we're not going to say which one
PHP Notice: Undefined variable: foo in /path/to/example.php on line 11
Done
Fatal errors are an important caveat to this. While a fatal error will still be caught by a custom error handler, PHP will log that fatal error regardless of the custom error handler returning true
or false
.
Custom error handlers are used to create customized error output. If you examine the parameters of the handler method, you’ll see PHP passes in all sorts of useful information we can use to create our own error messages, and better pin-point an error.
The other reason many frameworks set a custom error handler is to create a use strict
mode for PHP where all errors halt execution, not just fatal errors. Consider the following error handler
#File: example.php
function ourMagicErrorHandler($error_level, $error_string, $error_file, $error_line, $error_context)
{
echo $error_string,"\n";
echo "on line ", $error_line, "\n";
echo "in file ", $error_file, "\n";
exit(1);
// return false;
}
Running our script with this in place would produce the following output
$ php example.php
Undefined variable: foo
on line 14
in file /path/to/example.php
That is, a descriptive error message would be output, but execution would halt thanks to our exit
statement, despite this being an error PHP may normally recover from. Using an error handler like this when you’re developing code (vs. a production system) helps you fail fast, and prevents the slow growth of PHP code rot in your system.
Unfortunately, this sort of error handler often runs into trouble in the real world, as many useful PHP libraries produce myriad recoverable error messages. (Ask the Concrete 5 folks about ADODB)
Custom Error Handler Gotchas
There are, of course, some gotchas.
Per the manual,
The following error types cannot be handled with a user defined function:
E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING
, and most ofE_STRICT
raised in the file whereset_error_handler()
is called.
Since our custom error handler handles the errors at runtime, having the above errors in the same file as set_error_handler
would prevent set_error_handler
from being defined. Therefore there’s no way for a custom error handler to handle these errors in the same file.
Many novice, or even intermediate, developers get the idea to log errors in a custom error handler. While there’s nothing stopping you from doing this, the above caveat should make it clear that doing so will not catch all the errors in your system, and monitoring the real PHP error logs is still essential.
Checking for a Error Handler
When you start working with a PHP system you’re unfamiliar with, sometimes you’ll want to know if there’s a custom error handler running. Unfortunately, there’s no get_error_handler
function — instead you need to rely on some awkward code to get at the information you want.
When you set a custom error handler
$result = set_error_handler(array('Valid', 'callback'));
the return value for set_error_handler
is the error handler the function is replacing. If there’s no error handler set, set_error_handler
will return null
. So, to check for an error handler just set another one.
This, of course, means you’ve just changed the system behavior for the rest of the request (which may not be what you want). Fortunately, the restore_error_handler
function will let you undo the last call to set_error_handler
.
While it’s a little bit childish and stupid not the most intuitive pattern in the world, but then again, so is high school it does allow you to check for an error handler.
Exceptions
Some of the more object oriented among you may be scratching you heads and wondering
This all seems very procedural, and C/Go like. I thought PHP was a modern object oriented language with exceptions
Well, you’re right. This is very procedural and C/Go like. However, PHP does have exceptions — they just live alongside the PHP error system.
Like so many of the weirder things in PHP, this goes back to the transition from PHP 4 (with a basic object system) to PHP 5 (let’s make PHP a java-like OOP system). Exceptions were introduced in PHP 5, but with all the pre-existing legacy PHP code in the wild, there was little chance the PHP error system would be retired.
So, how do exceptions and errors co-exist? A caught exception won’t interact with the PHP error system at all. Catch you exceptions, handle them as your wish, and you won’t have anything to worry about.
However, an uncaught exception will create a fatal PHP E_ERROR
error, and php will halt execution (fatal errors are non-recoverable errors). One caveat: This uncaught exception fatal error will not be handled by a custom error handler.
The philosophy behind this is that an uncaught exception should always produce a crash, because exceptions should always be caught, and if they’re not it’s a bug in the program that needs to be fixed. Without any additional context, this makes sense — but that philosophy also assumes exceptions are your only method for handling errors.
Programming exception theory is complicated, controversial, and (in my opinion) ultimately dependent on the assumptions and theory-of-operation in your system. The Go language eschews exceptions in favor of error codes, and they have good, practical reasons for doing so. However, that decision doesn’t invalidate python’s decision to use exceptions to handle normal expected (i.e. unexceptional) error states. Both language designers have their reasons for those choices, and so long as they’re internally consistant and respectful of their philosophy, neither choice is “right” or “wrong”.
Unfortunately, given PHP’s “kitchen sink” theory of design, these sorts of confusing edge cases are inevitable. Should a fatal error caused by an exception be handled by a custom error handler? If you’re a native PHP developer, you probably assume it should — if you’re coming from Java, you probably assume it shouldn’t. If you’re coming from Go you’re like OMG why do I need to write PHP. Without an overriding theory of operation, these decisions can appear arbitrary — just a list of rules you need to memorize.
Further confusing things? There actually is a way to listen for uncaught exceptions with the set_exception_handler
function
function ourMagicExceptionHandler($e)
{
echo 'Someone threw an exception (type: <code>' .
get_class($e) . '</code>) and no one caught it.';
return true;
}
set_exception_handler('ourMagicExceptionHandler');
throw new Exception("Foo");
echo "Done";
However, while this function will allow you to “catch” an uncaught exception, the program will still terminate after running your handler code.
Confusing? Inconsistent? Counter intuitive? Multiple visions making a system overly complicated? From a certain point of view, yes, yes, yes and yes. That, unfortunately, is just how things are in PHP land. If you want PHP’s benefits, you need to live with PHP’s poor lack of design decisions. Hopefully the above is enough to help you come up with your own systems for dealing with errors and exceptions, or decipher the meaning behind the systems you’re been hired to work with.
Bitwise Constant System
As promised, we’re going to talk about PHP’s bitwise constant system. Our interest is in the error_reporting
constants, but PHP uses similar systems in many other places.
We’ll start with a quick CS-101 review of binary numbers. Unless you’re a robot or an alien, when you think of a number like, say 102
, you’re probably thinking in “base 10”. For example, the base 10 number 102
has
- A
2
in the ones place - A
0
in the tens place - A
1
in the hundreds place
for an end result of 102 different things. We typically don’t think of numbers in this way, but mathematicians need to because there’s number systems other than base 10. Of particular interest to computer science people are binary, or base 2 numbers. Instead of a ones place, tens place, hundreds place, etc., in base two a number has a ones place, a twos place, a fours place, an eights place, etc. So, the decimal number 102 is represented in binary as
1100110
That’s
0
in the ones place1
in the twos place1
in the fours place0
in the eights place0
in the sixteens place1
in the thirty-twos place1
in the sixty-fours place
So, that’s 64 + 32 + 4 + 2 = 102. Using just ones and zeros, we’ve represented one hundred and two different things.
The reason binary numbers are important to programmers is, at the end of the day, everything in a computer is ultimately represented in an on
or off
state. The RAM and the hard drives (or hard drives made of RAM) are all microscopic registers for storing a 1
, or storing a 0
. In this day and age you can write programs without being aware of these binary systems. However, at the dawn of computers, it would have been impossible to write a significant program without knowing about and manipulating these binary systems. Additionally, even in this day and age, there’s performance benefits to be gained by sticking to pure binary numbers.
PHP is implemented in C and C++, both languages forged during the dawn of computers. You may see where this is heading.
Earlier we talked about PHP’s error levels. What we glossed over was that each of these constants is actually a number. For the most recent version of PHP, these numbers are
1 E_ERROR (integer)
2 E_WARNING (integer)
4 E_PARSE (integer)
8 E_NOTICE (integer)
16 E_CORE_ERROR (integer)
32 E_CORE_WARNING (integer)
64 E_COMPILE_ERROR (integer)
128 E_COMPILE_WARNING (integer)
256 E_USER_ERROR (integer)
512 E_USER_WARNING (integer)
1024 E_USER_NOTICE (integer)
2048 E_STRICT (integer)
4096 E_RECOVERABLE_ERROR (integer)
8192 E_DEPRECATED (integer)
16384 E_USER_DEPRECATED (integer)
32767 E_ALL (integer)
In decimal (another word for base 10), these numbers don’t make much sense. Some numbers appear to be higher than others, so the lower the number the more serious the error? But E_PARSE
seems more serious than E_WARNING
. Besides that confusion, the gaps in the numbers seem arbitrary.
If we start to look at these numbers in binary, a pattern emerges.
000000000000001 E_ERROR (binary)
000000000000010 E_WARNING (binary)
000000000000100 E_PARSE (binary)
000000000001000 E_NOTICE (binary)
000000000010000 E_CORE_ERROR (binary)
...
100000000000000 E_USER_DEPRECATED (binary)
111111111111111 E_ALL (binary)
Each incremental error level shifts the 1
one column to the left in binary format. With that in mind, let’s consider our now ubiquitous variable undefined error
. The following program produces a Notice: Undefined variable: foo
error
<?php
error_reporting(E_ALL);
echo $foo;
This is as expected, since E_ALL
is the constant we’re supposed to use if we want all errors reported. Also, if we set a lower error reporting level
error_reporting(E_ERROR);
echo $foo;
no error will be produced. However, if we set the error level to E_CORE_ERROR
, which is one level higher than E_NOTICE
,
#File: example.php
<?php
error_reporting(E_CORE_ERROR);
echo $foo;
echo "Done\n";
we still won’t see any errors.
$ php example.php
Done
It’s only if we set the level to E_NOTICE
that an error will be produced.
error_reporting(E_NOTICE);
echo $foo;
The common misconception with PHP error levels is you’re telling PHP
Report errors this level or higher/lower
That’s not how the error constants work. Instead, each “place” in the binary number tells PHP which error we want to report. For example, if PHP says to itself
Hey, I need to issue a notice
It checks the binary version of the error reporting number, and looks at the fourth column
000000000001000
If the fourth column is a 1
, it produces the error. If the fourth column is a 0
, it does not produce an error. That’s why both E_CORE_ERROR
and E_ERROR
000000000000001 E_ERROR (binary)
000000000010000 E_CORE_ERROR (integer)
failed to produce a notice. Their fourth column/place was 0
, which PHP interpreted as “Don’t show a Notice
“.
Why create something so obtuse, and (seemingly) only able to display errors of one type? Because it’s actually a flexible and powerful system, and using binary numbers (theoretically) brings an efficiency to the PHP internals code. We’re just missing one key feature.
Let’s say you wanted to only show errors of type E_ERROR
or E_CORE_ERROR
. If you tried this
error_reporting(E_ERROR);
You’d miss the E_CORE_ERROR
errors. If you did this
error_reporting(E_CORE_ERROR);
You’d miss the E_ERROR
errors. However, if you consider the binary number
000000000010001
That is, a number with both the first (E_ERROR
) and the fifth (E_CORE_ERROR
) columns set to 1
, this number would tell PHP to show both the E_ERROR
AND the E_CORE_ERROR
errors. The binary number 000000000010001
can be represented as the decimal number 17
. So, if you set the error reporting level like this
error_reporting(17);
you’ll be set. With the first column set to 1
, PHP will report E_ERROR
errors. With the fifth column set to 1
PHP will report E_CORE_ERROR
errors. With every other column set to 0
, no other errors will be reported.
Of course, while powerful, the need to sit down and convert things to and from binary is a bit of the pain in the behind. Also, the PHP manual strongly discourages you from using raw integers in the error_reporting
function/ini
. Fortunately, you can take a shortcut. The above examples are equivalent to.
error_reporting(E_CORE_ERROR | E_ERROR);
If you var_dump(E_CORE_ERROR | E_ERROR)
, you’ll see the value of the expression E_CORE_ERROR | E_ERROR
is 17
. In english, the expression E_CORE_ERROR | E_ERROR
can be translated as
Report any error that’s an
E_CORE_ERROR
or anE_ERROR
As to why this works, we’ll need to take a detour into bitwise operators.
Bitwise Or
We’ve already talked a bit about how binary numbers are beloved by a certain type of programmer. These numbers are so beloved, there’s a group of special operators for dealing with binary numbers, similar to addition, subtraction, multiplication and division for all numbers. PHP, being the kitchen sink language that it is, supports these bitwise operators.
While it’s useful to learn all the bitwise operators, the one we’re going to talk about today is the “Or (inclusive or)” operator, represented by the |
character. Per the manual, this operator works on two numbers ($a
| $b
) and
Bits that are set in either $a or $b are set.
Still a little lost? Well, let’s consider two numbers, 17
and 24
. If you “bitwise inclusive or” these numbers
echo 17 | 24;
you’ll end up with 25
. When viewing these numbers as decimals, it makes no sense. However, if we view the numbers as binary
10001 (17 in base 10)
11000 (24 in base 10)
we have two “5 column” binary numbers, or two numbers with 5 bits each. A “bitwise or” returns bits that are set in either number, meaning a “bitwise or” of 10001
and 11001
10001
11000
------
11001
will return a 1
in the fifth, fourth, and first columns, or 11001
. The binary number 11001
is equivalent to the decimal number 25. Therefore, 17 | 24
= 25
.
Why do we care? Think back to the error_reporting
call
error_reporting(E_CORE_ERROR | E_ERROR);
Ah ha! It turns out we were using bitwise operators here. The expression E_CORE_ERROR | E_ERROR
is equivalent to 16 | 1
. If you consider the binary forms of these numbers
000000000000001 E_ERROR (binary)
000000000010000 E_CORE_ERROR (binary)
--------------------------------------------------
000000000010001 (our final result)
You can see how the “bitwise or” results in the number 10001
(or 17
in decimal). It also becomes clear why the specific numbers were chosen for each error level
000000000000001 E_ERROR (binary)
000000000000010 E_WARNING (binary)
000000000000100 E_PARSE (binary)
000000000001000 E_NOTICE (binary)
000000000010000 E_CORE_ERROR (integer)
...
By spacing the number as they did, the original PHP developers made it relatively easy to specify any set of errors you want with a “bitwise or”. If you wanted to turn on all the PHP errors, you could do something like this
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE | E_CORE_ERROR | E_CORE_WARNING |
E_COMPILE_ERROR | E_COMPILE_WARNING | E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE | E_STRICT |
E_RECOVERABLE_ERROR | E_DEPRECATED | E_USER_DEPRECATED);
By combining all the errors with a bitwise operator, we end up with a binary number of
111111111111111
which is equal to 32767
, which (in no coincidence) is also the value of the E_ALL
constant. E_ALL
is a shortcut for adding all the errors, and not an error level in and of itself.
A few of you may be wondering about the following statement, used earlier in this article
error_reporting(E_ALL | E_STRICT);
This is needed for PHP 5.3. In PHP 5.3 E_ALL
didn’t include E_STRICT
errors for backwards compatibility reasons. In our new mathematical language, in PHP 5.3 E_ALL
was equal to 30719
In binary, that’s
111011111111111
Notice anything? The twelfth column is a 0
. This is the column that controls E_STRICT
errors. E_STRICT
is equal to 2048
, or 000100000000000
in binary. Line up the two numbers for a “bitwise or”
111011111111111
000100000000000
----------------
111111111111111
and you get the magic “SHOW US ALL THE ERORS” number, 111111111111111
. In later versions of PHP (5.4+), this E_STRICT
special case was removed, and E_ALL
was changed to 111111111111111
, ensuring E_ALL
truly contains ALL the errors.
Error Miscellany
While we’ve put a dent in the surface of PHP’s various error systems, there’s still plenty of other bits of miscellany to trip up any newcomer. The rest of this article will cover some of the more common cases you may need to deal with.
Runtime vs. php.ini
Many of the configuration settings that control error handling can be set via a php.ini
file, or via runtime functions like error_reporting
or ini_set. While the runtime functions are incredibly useful, they do create an enormous amount doubt when you’re seeing some weird behavior but no corresponding PHP error message on the screen or in a log. Since the way PHP handles errors can change at anytime, you’re never 100% sure you’re seeing all the errors.
As mentioned previously, my way of dealing with this is to temporarily add the following code as close as I can to the potential error.
error_reporting(E_ALL | E_STRICT);
ini_set('log_errors','1');
ini_set('error_log','/tmp/my-custom-php-error-log.log');
ini_set('display_errors', '1');
However, this isn’t 100% fool proof. PHP has the ability for an administrator to disable functions. Many managed (or managed-ish) hosting companies will disable the ini_set
function, or even the error_reporting
function as a security precaution. Outside of this feature, there’s a slew of public and private PHP security extensions that limit what can, and can’t, be set at runtime.
Just one more piece of trivia to keep under your hat if you’re a PHP developer.
Finding php.ini
Sometimes, especially when you’re dealing with an unfamiliar system, it’s difficult to track down the php.ini
file you’re looking for. This is complicated by the fact PHP can load multiple ini files. The location of PHP’s ini
file is set when PHP is compiled (it’d be pretty hard to have an ini
setting for the location of php.ini
, no?).
In addition to this main php.ini
, PHP will also scan an extra directly (also set at compile time) for additional files ending in ini
. If any are found, PHP will load them after it loads the main php.ini
file.
Fortunately, PHP has a mechanism for displaying these ini files. If you’re running PHP from the command line, just pass in the --ini
flag
$ php --ini
Configuration File (php.ini) Path: /usr/local/php5/lib
Loaded Configuration File: /usr/local/php5-20130127-205115/lib/php.ini
Scan for additional .ini files in: /usr/local/php5/php.d
Additional .ini files parsed: /usr/local/php5/php.d/10-extension_dir.ini,
/usr/local/php5/php.d/50-extension-apc.ini,
/usr/local/php5/php.d/50-extension-curl.ini,
/usr/local/php5/php.d/50-extension-intl.ini,
/usr/local/php5/php.d/50-extension-mcrypt.ini,
/usr/local/php5/php.d/50-extension-memcache.ini,
/usr/local/php5/php.d/50-extension-memcached.ini,
/usr/local/php5/php.d/50-extension-mongo.ini,
/usr/local/php5/php.d/50-extension-mssql.ini,
/usr/local/php5/php.d/50-extension-oauth.ini,
/usr/local/php5/php.d/50-extension-pdo_dblib.ini,
/usr/local/php5/php.d/50-extension-pdo_pgsql.ini,
/usr/local/php5/php.d/50-extension-pgsql.ini,
/usr/local/php5/php.d/50-extension-solr.ini,
/usr/local/php5/php.d/50-extension-twig.ini,
/usr/local/php5/php.d/50-extension-uploadprogress.ini,
/usr/local/php5/php.d/50-extension-xdebug.ini,
/usr/local/php5/php.d/50-extension-xhprof.ini,
/usr/local/php5/php.d/50-extension-xsl.ini,
/usr/local/php5/php.d/50-extension-xslcache.ini,
/usr/local/php5/php.d/99-liip-developer.ini
and you’ll get a list of every php.ini
file loaded, as well as their location.
If you’re running PHP from a web server context, the phpinfo()
function will output similar information
Of course, that’s the simple case — and nothing is ever that simple with PHP.
Earlier in this article we covered setting php.ini
settings in an .ini
file, and at runtime with the ini_set
functions. It’s also possible for ini
settings to be set in the web server configuration. Assuming you’re using apache, this means in httpd.conf
as well as .htacess
with syntax like this
php_value display_errors 1
Additionally, PHP 5.3 introduced the .user.ini
files. This system allows you to create a .user.ini
file in a web server directory, and PHP will parse this file for ini values. Despite ending in .ini
, the format for this file is the same as the .htaccess
format listed above.
Finally, while not strictly a php.ini
file, PHP has a special ini
configuration value name auto_prepend_file
. This setting allows you specify a PHP file to be parsed before the main PHP file is parsed. While you can drop anything you like into this file, I’ve encountered many multi-user setups where each individual developer has their own auto_prepend_file
to set developer only configuration settings at runtime. If a full path isn’t used for this value, PHP will search the include_path
for a file.
Custom PHP Errors
PHP has a function you can call to trigger your own errors. By default these errors are issued at the E_USER_NOTICE
level, but you can change that using the method’s second parameter.
trigger_error('This is a custom notice.',E_NOTICE);
The trigger_error
function was introduced in the PHP 4 days to give PHP some exception-like, (or is that lite?), behavior — of course now that PHP has an exception system this secondary system may seem redundant to some, while others may find it useful to use in concert with the exception system. This message from the manual makes it clear where the PHP documentation teams falls on that subject
This function is useful when you need to generate a particular response to an exception at runtime.
Or maybe the PHP documentation team isn’t using exception to mean a PHP exception, but instead as the english language “something out of the ordinary happened” usage.
Programming is hard kids — don’t let anyone tell you otherwise.
Additional Error Logging
There’s two additional error logging functions a PHP developer should be aware of: error_log
and syslog
.
Per the manual, the error_log
function “Send[s] an error message to the defined error handling routines”. This doesn’t mean PHP will raise a PHP error. If you’re running PHP in a web context, the message is sent directly to the error log. If you’re running PHP in a command line context, calling error_log
will also send a message to the error log, unless the error log isn’t set. If the error_log
ini
isn’t set, then the command line PHP will send the message to STDERR
.
The error_log
function will perform its duties regardless of the value set in the log_errors
ini setting. Because of this, error_log()
is often used by frameworks to ensure a message is sent to the error log. Despite ignoring log_errors
, this function will respect the error_log
ini that sets a log file location, (defaulting to the web server log if no value is set in error_log
.
The syslog
function is similar, in that it sends a messages directly to a logging system. However, instead of the PHP error log, or a web server error log, this function sends a message directly to the operating system’s system log. If you’re interested in learning more about this, checkout the manual page
$ man syslog.conf
You’ll mostly see this used in private PHP frameworks where one of the developers is also a unix system administrator and wants all their messages in the same place. Most modern (or even middle aged) PHP frameworks tend to abstract their own systems away from a specific system like *nix
‘s syslogd
.
Errors During eval
PHP, like most “end user-programmers don’t need to compile things” languages has an eval function. An eval
function lets a user create a string that’s valid PHP code, and then run the string as a mini-program. There’s security concerns around the use of eval
which make it one of those hot button topics in programming circles, but we’re not here to talk about that. If you’ve decided to use eval
, or your framework-of-choice-or-fiat uses it, there’s a few things w/r/t to errors you’ll need to keep in mind.
An error in a string of eval
ed code will still be sent to a custom error handler set in the main PHP program with set_error_handler
.
However, when PHP creates error messages for the eval
ed code, the line number referenced in that error string will refer to the line in the eval’d block, and not the line number in the main program where eval
is called. For example, this code
<?php
echo "Foo";
echo "Baz";
eval('echo $foo;');
will issue the following error message
PHP Notice: Undefined variable: foo in /path/to/example.php(4) : eval()'d code on line 1
The line 1
refers to the line in the eval
string. If you want the line of the PHP program where eval was called, look for the number in parenthesis (in this case 4
)
Also, remember how a custom handler can’t handle certain error types (including E_PARSE
) generated in the same file? This is true even if the code executes in an eval
block.
The Last Error: $php_errormsg
As mentioned earlier, there are many errors which PHP can recover from. After handling an error, PHP will populate a variable named $php_errormsg
in the current scope, ensuring $php_errormsg
always contains the latest error, (unless you leave that current scope, of course) I’ve seen many custom error handling routines that make some use of this variable, so you’ll want to be aware of it.
Also, if you’re using set error handler and return true
(meaning you’re skipping the default PHP error handling), then PHP will not populate this variable.
Another caveat: The php.ini
setting track_errors
must be true
for this variable to be populated.
The Error Control Operator
PHP has an error control/surpression operator. Place the operator before any code that might cause an error, and PHP will swallow the error whole. For example, running this script should produce an undefined variable foo
Notice
function main()
{
echo $foo;
}
@ main();
But because of the leading @
, no error is produced. Unfortunately, these errors are almost completely swallowed. They’re not displayed, and not logged.
The only trace the error happened will be the population of the previously mentioned $php_errormsg
variable. So, in theory, it’s still possible to detect and process a suppressed error, but given $php_errormsg
is only available for the scope in which the error occurred, and it’s impossible to tell when the error in $php_errormsg
was generated without extra coding gymnastics, the usefulness of this construct is limited.
Most professional PHP developers I know and respect eschew the @
handler whenever possible. Unfortunately, given a 10 second code change will completely swallow an error that might take an entire day to track down, you’ll often see code bases littered with the @
operator. These are also the projects that, inevitably, end up producing the most bizarre and hard to track down errors, given the undetectable-unless-you’re-already-looking-at-it nature of @
.
Error Control and Custom Error Handlers
The other way an error control operator suppressed error might show up is in a custom error handler. Your custom error handler will be called for a suppressed error. However, per the manual
If you have set a custom error handler function with
set_error_handler()
then it will still get called, but this custom error handler can (and should) callerror_reporting()
which will return 0 when the call that triggered the error was preceded by an @.
If you call the error_reporting
function without passing in a level, it will return the current error reporting level. However, the error control operator changes this behavior, and within your custom error handler error_reporting
will be reported as 0
if the error control operator was used. The manual text quoted here suggests your custom error handler should respect this setting. Most of the custom error handlers I’ve seen in the wild do not respect this setting. “The right” thing to do here is left as an exercise for the reader.
Backtrace
While not strictly an error handling mechanism, the php functions debug_backtrace
and debug_print_backtrace
are often used in a custom error handler to print out a PHP call stack. A PHP call stack will list the chain of methods/functions called to reach a particular depth/point in a program — which is useful debugging information.
Be careful using these functions in systems where objects have circular references, or contain a massive amount of information. The call stack, by default, contains a dump of every parameter variable used, and the display of circular object references, or just large objects, will eat up all the memory available unless you’re using something like xDebug to control the depth displayed
xdebug.var_display_max_depth = 3
What’s xDebug
Speaking of which, if you’re doing any sort of PHP development work (as opposed to developing within a PHP based system), you probably want to install xDebug. While a full feature rundown of xDebug
is beyond the scope of this article, xDebug does alter PHP’s handling of error messages. With xDebug enabled, every PHP error will include a full call stack of the code that led to the error. For example, without xDebug the following program
#File: example.php
<?php
function main()
{
echo $foo;
}
main();
would produce the error
Notice: Undefined variable: foo in /path/to/example.php on line 4
However, with xDebug installed, the error would read
Notice: Undefined variable: foo in /path/to/example.php on line 4
Call Stack:
0.0003 635008 1. {main}() /path/to/example.php:0
0.0004 635008 2. main() /path/to/example.php:6
The last item in the call stack
2. main() /path/to/example.php:6
is the function/method (main
) where the error occurred, including a file (example.php
) and line number (:6
). The line number is where the function was called, not where the error occurred. You can find the line number where the error occurred in the original message
Notice: Undefined variable: foo in /path/to/example.php on line 4
In addition to this call stack, xDebug will wrap the displayed errors in a garish orange HTML rectangle to help draw a developer’s attention to them.
Wrap Up
As you can see, with over 18 years of constant but backwards compatible development, PHP error’s handling features are broad, powerful, and incredibly inconsistant. While this article puts a dent in the topic, there’s a lot it doesn’t cover. (Have a favorite we didn’t mention? Drop it in a comment below or get in touch.
For day to day programming, you shouldn’t need to dive this deep into error handling. However, if you’re planning on making programming a long term part of your career, the ability to diagnose foreign systems is a must have skill. This means knowing every nook and cranny of what’s possible, even if it’s something you’d never do yourself.