- A Survey of PHP Error Handling
- Magento’s Mini Error Framework
- Laravel’s Custom Error Handler
Laravel’s error handling is one of its most noticeable developer facing features. Like many modern PHP frameworks, Laravel ships with its error reporting set to E_ALL
, and strives for Notice
free PHP code. Additionally, Laravel includes the Whoops framework, which creates readable stack traces that pinpoint the exact line a PHP error came from.
Where Laravel really distinguishes itself is the steps it takes, on a framework level, to make sure its error handling accounts for all PHP errors. PHP has uncaught exceptions, fatal errors, caught exceptions, and non-fatal errors — making sure you catch every possible “bad” code situation is harder than it sounds, and it’s a credit to the Laravel core team that they’ve managed to thread this needle.
This article with explore Laravel’s systems level error handling code, as well as the myriad ways PHP makes robust error reporting difficult to accomplish.
Bootstrap
Laravel, like most modern PHP frameworks, is built around the idea of a single global application object. Laravel redirects all requests to a central index.php
file. If you omit the comments, this file is pretty sparse
#File: index.php
require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/start.php';
$app->run();
There’s a line to pull it Laravel’s custom autoloading rules, a line to pull in an application bootstrap start.php
file, and a call to the application’s run
method. The bootstrap start file is where the Laravel core instantiates the application file, and then returns the application object
#File: bootstrap/start.php
$app = new Illuminate\Foundation\Application;
//...
return $app;
If you’re used to a more class based PHP framework, you may be surprised to learn you can return a value from a PHP include
or require
statement.
It’s a good idea to become familiar with everything going on in this file, but the line we’re interested in today is here
#File: bootstrap/start.php
$framework = $app['path.base'].
'/vendor/laravel/framework/src';
require $framework.'/Illuminate/Foundation/start.php';
Here Laravel pulls in another file named start.php
. Why the second bootstrap? You’ll notice this second file exists in the composer vendor hierarchy — that is, it’s a file distributed with the core packages that make up the Laravel framework. The bootstrap/start.php
file, on the other hand, lives under the root project folder. Once Laravel creates its initial application, this first bootstrap file is the responsibility of the application owner/developer. That is, once created, composer or Laravel will never update it again.
Having the first start.php
perform a require
on the second start.php
ensures the application developer can take advantage of future improvements to the framework without needing to merge a file that’s part of their application.
Like the first start.php
, there’s a lot of important things going on in this second Illuminate foundation start.php
file. The lines we’re interested in are these two.
#File: vendor/laravel/framework/src/Illuminate/Foundation/start.php
$app->startExceptionHandling();
if ($env != 'testing') ini_set('display_errors', 'Off');
The startExceptionHandling
method on the application object is where Laravel setups up its custom error handlers. You’ll notice it’s using the global $app
object — this is the same object setup in the previous include file. Remember — unless a PHP includes/require file starts with a namespace declaration, it exists in its parent’s scope, and in this case that’s PHP’s global scope.
Before we dive into the startExceptionHandling
method, make note of the second line.
#File: vendor/laravel/framework/src/Illuminate/Foundation/start.php
if ($env != 'testing') ini_set('display_errors', 'Off');
This is where Laravel turns off displaying errors during a test run. It’s also an example of the Laravel core developers showing a strong opinion, and using the more-concise-but-prone-to-errors single line bracket-less conditional statement.
A less opinionated developer might write the above like like this
if ($env != 'testing')
{
ini_set('display_errors', 'Off');
}
We point it out here mainly so you’ll be aware it’s a common site in the Laravel code base, so adjust your code smell sensors accordingly.
The Laravel Application Container
If we take a look at the application object’s startExceptionHandling
method,
#File: vendor/laravel/framework/src/Illuminate/Foundation/Application.php
public function startExceptionHandling()
{
$this['exception']->register($this->environment());
$this['exception']->setDebug($this['config']['app.debug']);
}
we see the actual logic is handled by the object in the exception
prop– except, wait? How is $this['exception']
even working? Shouldn’t that throw a Cannot use object as array error? The reason this works in Laravel’s Application
object extends a base Laravel container class, and the container class implements the PHP ArrayAccess interface.
#File: vendor/laravel/framework/src/Illuminate/Container/Container.php
//...
class Container implements ArrayAccess {
}
If you’re not familiar with it, implementing the ArrayAccess
interface allows you to programtically control what happens when you use array syntax ($foo[2]
) with a PHP object.
Covering this in full is beyond the scope of this article, but when you see $this['someprop']
in the application object, Laravel’s accessing a service object. Don’t worry if you don’t understand Laravel’s services — that’s a topic for another day. For the purposes of this article, all you need to know is $this['exception']
contains an object who’s class is Illuminate\Exception\Handler
, and that’s where we’ll find the PHP code used to setup Laravel’s error handling.
Configuring PHP’s Error Handling
Let’s take the two lines of PHP above
#File: vendor/laravel/framework/src/Illuminate/Foundation/Application.php
$this['exception']->register($this->environment());
$this['exception']->setDebug($this['config']['app.debug']);
and consider the second line first. The setDebug
method
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
public function setDebug($debug)
{
$this->debug = $debug;
}
is just a simple setter method that allows the exception service to know the value of the app.debug
configuration field. If you’ve ever wondered why changing this value at runtime doesn’t seem to affect the error display, this is why. Once Laravel sets the debug
property, it never references the configuration for this value again.
Next up is the method we’re really interested in: register
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
public function register($environment)
{
$this->registerErrorHandler();
$this->registerExceptionHandler();
if ($environment != 'testing') $this->registerShutdownHandler();
}
Here we see Laravel’s calling three different methods — registerErrorHandler
, registerExceptionHandler
, and registerShutdownHandler
. For folks new to the platform, PHP has both an error system and an exception system. Exceptions were introduced to the language in PHP 5, but the error system was left in place for backwards compatibility reasons.
If we take a look at the definitions for each of these methods.
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
protected function registerErrorHandler()
{
set_error_handler(array($this, 'handleError'));
}
protected function registerExceptionHandler()
{
set_exception_handler(array($this, 'handleUncaughtException'));
}
protected function registerShutdownHandler()
{
register_shutdown_function(array($this, 'handleShutdown'));
}
We can see they’re using the native PHP functions to register callback methods. A PHP callback is a pseudo type that PHP knows how to invoke as a method or function. A callback can be a string (invoked as a function), a first-class-function/closure (invoked as itself), or, as above, an array where the first item is an object, and the second item is an object method.
If you didn’t follow all that, what it means is when we say
set_error_handler(array($this, 'handleError'));
We’re telling PHP to call $this->handleError()
as the custom error handler. Similarly, when we say
set_exception_handler(array($this, 'handleUncaughtException'));
We’re telling PHP to call $this->handleUncaughtException()
as the custom exception handler. The same is true for $this->handleShutdown
and the register shutdown function. Don’t worry if you’re not familiar with the shutdown function — we’ll be talking about it more below.
Tracing an Error
So that’s the exception and error handler set, but what actually happens when your Laravel application issues an error or doesn’t catch an exception?
First, let’s take a look at the error handler
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
public function handleError($level, $message, $file = '', $line = 0, $context = array())
{
if (error_reporting() & $level)
{
throw new ErrorException($message, 0, $level, $file, $line);
}
}
Here we see that Laravel turns all errors into a thrown ErrorException
exception. Actually, that’s a Illuminate\Exception\ErrorException
object (since we’re in Illuminate/Exception/Handler.php
, and the namespace is Illuminate\Exception
). This means almost every error and warning in PHP will also be routed to the Exception handler (unless, of course, application code does a try/catch for their PHP errors).
We say “almost every” because, as you can see above, the exception throwing code is wrapped in a conditional. Look carefully at that conditional
error_reporting() & $level
It’d be easy to read that as “If error_reporting()
AND $level
are true, but that’s not what’s going on. That’s a single &
operator — which is one of PHP’s [bitwise operators] (http://php.net/manual/en/language.operators.bitwise.php). If you’re not familiar with bitwise operators and how they relate to PHP’s error handling, I’ve written extensively on this in my Survey of PHP error handling article.
For our purposes, know that if (error_reporting() & $level)
can be translated into english as “If the error that just happened is an error PHP would report”. This means Laravel’s error handler will obey your custom error levels (see the shutdown handler section below for a caveat to this). It’s also worth mentioning that back up in the Illuminate Foundation Bootstrap start.php
, Laravel sets the error handling to -1
#File: vendor/laravel/framework/src/Illuminate/Foundation/start.php
/*
|--------------------------------------------------------------------------
| Set PHP Error Reporting Options
|--------------------------------------------------------------------------
|
| Here we will set the strictest error reporting options, and also turn
| off PHP's error reporting, since all errors will be handled by the
| framework and we don't want any output leaking back to the user.
|
*/
error_reporting(-1);
An error reporting level of -1
is a special shortcut that means show all PHP errors. It’s functionally equivalent to error_reportin(E_ALL)
, although the reason -1
works is, again, based on PHP’s bitwise error codes.
So, in short, the Laravel error handler will throw an ErrorException
whenever PHP issues a reportable error. This means our next stop is the uncaught exception handler.
Tracing an Exception
If we take a look at the exception handling callback, we see
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
public function handleUncaughtException($exception)
{
$this->handleException($exception)->send();
}
That line might be more clearly written as
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
$this->handleException($exception)
->send();
Or even
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
$response = $this->handleException($exception);
$response->send();
That is, the exception handler calls another method (handleException
), and then calls send
on the object returned by the handleException
method. Before we hop over to the handleException
method, we’ll let you know this call returns a Laravel response object, and the send
method will send output back to the browser/user.
With that in mind, here’s handleException
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
public function handleException($exception)
{
$response = $this->callCustomHandlers($exception);
// If one of the custom error handlers returned a response, we will send that
// response back to the client after preparing it. This allows a specific
// type of exceptions to handled by a Closure giving great flexibility.
if ( ! is_null($response))
{
return $this->prepareResponse($response);
}
// If no response was sent by this custom exception handler, we will call the
// default exception displayer for the current application context and let
// it show the exception to the user / developer based on the situation.
return $this->displayException($exception);
}
The first two lines
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
$response = $this->callCustomHandlers($exception);
if ( ! is_null($response))
{
return $this->prepareResponse($response);
}
reveal an important feature of the Laravel application object. Laravel’s application object has a pushError
method. This method allows you to register callbacks to handle specific Exception
types yourself. Give the following code a try in your application
app()->pushError(function($exception, $status_code, $is_this_error_from_the_console){
var_dump(__FILE__ . '::' . __LINE__);
//var_dump(func_get_args());
return 'I am the exception handler now.';
//return null; #return null if you don't want to handle it
});
throw new Exception('Look at me');
We’re not going to dive into the implementation of this feature, except to say callCustomHandlers
is the method that goes through and calls all the custom errors pushed onto the application, and the call to $this->prepareResponse($response)
takes the value returned by the custom error handler and turns it into a Laravel response object.
Assuming there’s no custom error handler, the next journey for our exception is into the displayException
method.
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
$this->displayException($exception);
This method is responsible for displaying the exception. Well, actually, if we look at its definition
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
protected function displayException($exception)
{
$displayer = $this->debug ? $this->debugDisplayer : $this->plainDisplayer;
return $displayer->display($exception);
}
we see its responsible for calling display
on the Exception handler’s “displayer” object. The first line
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
$displayer = $this->debug ? $this->debugDisplayer : $this->plainDisplayer;
chooses which displayer object the exception handler should use. If the debug
property is set on the exception handler, the exception handler will use the debugDisplayer
. If not, it uses the plainDisplayer
. Once Laravel chooses the displayer, it calls its display
method (passing in the exception).
return $displayer->display($exception);
In english, this means if you’re running Laravel with the debug
configuration set to true
, you’ll get the super fancy “Whoops” exception handler
but if you’re running with the debug
configuration set to false
, you’ll get the stock Laravel production debug message.
It’s beyond the scope of this article, but if you’re interested in how Laravel sets up the debug
property, as well as the debugDisplayer
and plainDisplayer
, take a look in the register
method of the Exception service provider
vendor/laravel/framework/src/Illuminate/Exception/ExceptionServiceProvider.php
Also beyond the scope of this article is how the Whoops exception handler “does its magic”. If you’re curious in tracing that our yourself, by default the debugDisplayer
‘s class is Illuminate\Exception\WhoopsDisplayer
, defined in the following file
vendor/laravel/framework/src/Illuminate/Exception/WhoopsDisplayer.php
All that said, we will take a quick look at how the production/plain displayer works, since it’s relatively simple
#File: vendor/laravel/framework/src/Illuminate/Exception/PlainDisplayer.php
class PlainDisplayer implements ExceptionDisplayerInterface {
public function display(Exception $exception)
{
$status = $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : 500;
$headers = $exception instanceof HttpExceptionInterface ? $exception->getHeaders() : array();
return new Response(file_get_contents(__DIR__.'/resources/plain.html'), $status, $headers);
}
}
As you can see above, Laravel will load the HTML contents of the following file as a string
./vendor/laravel/framework/src/Illuminate/Exception/resources/plain.html
and use this file’s contents to generate a Laravel response object. It’s important to note that this plain.html
file exists as part of a vendor
package — which means any change you make to it will be overridden when you update the framework. This means an application developer has no way to customize the display of the PlainDisplayer
.
Fortunately, Laravel provides a method (via the App
facade) to add a custom error handler.
App::error(function(){
//if this isn't production, bail
if(App::environment() !== 'production')
{
return;
}
return '<p>Any valid Laravel Response here</p>';
});
This is very similar to the pushError
method, with one important difference. The pushError
method adds an error handler to the end of the error handling queue. The more common error
method will add your new error as the first error handler. This gives you the opportunity to preempt any other error handlers registered in your system.
So — that’s the exception handler traced out. Through this process Laravel creates and sends a response object for any uncaught exception. Any error message that would normally display an error is turned into an exception. This handles a proverbial 99% of all error cases in PHP — but there’s one last thing we need to consider — and that’s the shutdown handler.
Tracing the Shutdown Handler
You’ll remember back up in the register
method, there was a final line where Laravel setup a shutdown handler.
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
if ($environment != 'testing') $this->registerShutdownHandler();
//...
protected function registerShutdownHandler()
{
register_shutdown_function(array($this, 'handleShutdown'));
}
A shutdown callback isn’t specifically for handling errors. Instead, it’s a callback that fires when a PHP page/program has finished its execution. However, if we take a look at handleShutdown
, we’ll see Laravel’s using it for error/exception handling
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
public function handleShutdown()
{
$error = error_get_last();
// If an error has occurred that has not been displayed, we will create a fatal
// error exception instance and pass it into the regular exception handling
// code so it can be displayed back out to the developer for information.
if ( ! is_null($error))
{
extract($error);
if ( ! $this->isFatal($type)) return;
$this->handleException(new FatalError($message, $type, 0, $file, $line))->send();
}
}
The shutdown function uses PHP’s error_get_last
function to fetch an array of information about the last error. A null
response indicates there hasn’t been an error — if that’s the case handeShutdown
finishes without taking additional action. However, if error information is returned, and the error type is one of PHP’s fatal errors
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
protected function isFatal($type)
{
return in_array($type, array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE));
}
then Laravel will instantiate a FatalError
object, and hand this object off the the handleException
method, same as an uncaught exception from the exception handler. (The variables in the function above are defined by the call to extract — required reading if you’re not familiar with extract
).
So — why is this here? Let’s go back and consider the error handler
#File: vendor/laravel/framework/src/Illuminate/Exception/Handler.php
public function handleError($level, $message, $file = '', $line = 0, $context = array())
{
if (error_reporting() & $level)
{
throw new ErrorException($message, 0, $level, $file, $line);
}
}
You’ll recall this method throws an exception for any error that PHP would normally display — but what happens if your program produces a fatal error, but your error reporting is set to not report an error on that level? In this scenario the handleError
method wouldn’t throw an exception — and PHP would do no error reporting. Since the error is fatal this halts PHP’s page rendering with no notice. Some PHP developers refer to this as the “white screen of death”. That’s far from ideal, so Laravel has the shutdown function there to catch any fatal errors.
Put another way, Laravel attempts to obey the system’s error_reporting
level unless it’s a fatal error, in which case Laravel attempts to handle the error. I don’t have enough history with the platform to know why this behavior is implemented in a shutdown function instead of in the error handler itself, but my guess would be the shutdown function came as an enhancement/bug-fix later in the framework’s life.
The shutdown function also serves another purpose — there’s a few not-well-documented situations where PHP will raise an error, but not call any custom error handling functions. Some of these are impossible to report on due to the nature of the error (PHP itself seg-faults), but if, for any reason, Laravel’s error and exception handlers fail to catch an error, this shutdown handler is there to grab it.
One negative consequence of this behavior is it’s impossible to turn off Laravel exception handling in the framework without a wholesale redefinition of the application object. That’s because once a shutdown handler is registered, the PHP API provides no way to cancel or change it.
Wrap Up
As you can see, thanks to years of legacy behavior, doing something seemingly simple like controlling how PHP handles all its errors can be a herculean task. While Laravel’s approach isn’t perfect, it is some of the best I’ve seen from a framework, and their use and promotion of whoopos is a clear win for developers. If you’re thinking of creating a new PHP framework, you could do a lot worse than following the lead set by the Laravel core team.