Error Handling
Table of contents
Loading...Introduction
PHP is an "exception-light" programming language, meaning that it doesn't throw exceptions for every little thing that goes wrong unlike other languages such as Python.
The chapter Errors and Exceptions from PHP The Right Way explains in great detail how PHP handles errors.
The following does not throw an exception but a warning:
Warning: Undefined variable $foo
.
"Hello world" is still printed.
<?php
echo $foo;
echo 'Hello world';
PHP categorizes errors into different levels of severity, with three primary types of
messages: errors, notices, and warnings.
Each type corresponds to a specific constant, namely E_ERROR
, E_NOTICE
, and E_WARNING
.
The list of all error constants can be found here.
Fatal errors
Errors with the severity E_ERROR
are fatal and result in the termination of the script execution.
They occur when there is a critical error, for e.g. a syntax error or an undefined function.
Warnings and notices
Notices (E_NOTICE
) and warnings (E_WARNING
) don't halt the script execution.
They produce a message and continue processing.
Notices are advisory messages that occur when PHP encounters something that might be an error, and warnings are non-fatal errors.
Configuration
Displaying error details to the user exposes information about the structure of the application. In development, it's useful to see the errors to debug the application, but in production, it would present a big security risk.
Error configuration is done in the php.ini
file on the webserver and
in the application itself if it implements an error handler.
If the error handler from the application picks up the error, the application configuration
overrides the php.ini
settings but if the error happens before the error handler is initialized,
the server configuration is used.
This means that it's important to configure the php.ini
file correctly even if the
application error handler is configured separately.
Server configuration
By default, (apache) errors are logged in the error.log
file on the webserver.
Here is a summary of the error configuration options for a PHP server:
- error_reporting: What levels of errors get triggered.
- display_errors: Whether to show triggered errors in script output.
- display_startup_errors: Whether to show errors that were triggered during PHP's startup sequence.
- log_errors: Whether to write triggered errors to a log file.
error_reporting
should always be set to E_ALL
except if certain error severities should be
ignored.
Development settings
To show every possible error during development, the php.ini
file should be configured like this:
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
log_errors = On
This will show the error details to the user either by the default PHP error message, the framework default error handler, the debugging tool (e.g. xdebug) or a custom error handler.
Production settings
In production, no error detail should ever be made visible to the user.
File: php.ini
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On
This will take into account the errors, log them and display a generic error page but not show the details.
Application configuration
The error configurations that are used by the Slim error handler are initialized in the
defaults.php
file and then modified in the
environment configuration files.
File: config/defaults.php
$settings['error'] = [
// Must be set to false in production
'display_error_details' => false,
// Whether to log errors or not
'log_errors' => true,
];
Development
All errors, warnings and notices should be displayed in the browser while developing.
Therefore, display_error_details
should be true
in the development config file.
File: config/env/env.dev.php
// Display error details in browser and throw ErrorException for notices and warnings
$settings['error']['display_error_details'] = true;
Production
Errors, warnings and notices should be logged, but details shouldn't be shown to the client in production.
File: config/env/env.prod.php
// Display generic error page without details
$settings['error']['display_error_details'] = false;
Testing
During testing, every notice and warning should bring the test to a halt, so
display_error_details
should be true
.
File: config/env/env.test.php
// Enable display_error_details for testing as this will throw an ErrorException for notices and warnings
$settings['error']['display_error_details'] = true;
// Disable error logging but this shouldn't be necessary as the test setup should be configured to never log errors
$settings['error']['log_errors'] = false;
Default Error Handling in Slim
The default way to handle errors in Slim is by adding the ErrorMiddleware
with the addErrorMiddleware
method or when a bit more control over the configuration is required,
the ErrorMiddleware
can be defined in the container and then added to the stack.
The latter is required to take the environment configuration into account.
Container instantiation
The ErrorMiddleware
is instantiated in the
container
with the config values and
logger.
File: config/container.php
use Slim\Middleware\ErrorMiddleware;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Slim\App;
return [
// ...
ErrorMiddleware::class => function (ContainerInterface $container) {
$config = $container->get('settings')['error'];
$app = $container->get(App::class);
$errorMiddleware = new ErrorMiddleware(
$app->getCallableResolver(),
$app->getResponseFactory(),
(bool)$config['display_error_details'],
(bool)$config['log_errors'],
true, // log error details
$container->get(LoggerInterface::class)
);
return $errorMiddleware;
},
];
Add to middleware stack
The ErrorMiddleware
must be added at the very end of the middleware stack.
File: config/middleware.php
use Slim\App;
use Slim\Middleware\ErrorMiddleware;
return function (App $app) {
// ...
// Last middleware in the stack
$app->add(ErrorMiddleware::class);
};
Conclusion
This is how to add the default error handling middleware to the Slim application.
This setup might be sufficient for some applications, but in my opinion, lacks some crucial
features to make the error handling more user-friendly and informative.
Thankfully, with Slim and PHP, it's straightforward to implement a custom error handler.
Custom Error Handler
The way errors are logged and displayed to the user can be customized with a custom error handler.
Symfony has a great error page that displays the error details in a clear and pretty format. Laravel uses the whoops, which also displays error details and code.
For my projects, I wanted an error handler that renders a lean error details page that highlights the important files in an uncluttered stack trace.
I've extracted the error handling into a small library
slim-error-renderer
that provides a custom error handling middleware and renderer as well as a
middleware to promote notices and warnings to exceptions.
The documentation on how to add the middlewares to your project can be found in the
installation guide
of the package.
The setup is similar to the default error middleware in Slim.
Below is a guide on the key elements of the slim-error-renderer
library which can be used to
create an own custom error handler or to understand how the error handling works in Slim.
Error handling middleware
The core of the
ExceptionHandlingMiddlware
is a try
catch
block that catches all exceptions and invokes the custom error handler.
It's essential that ExceptionHandlingMiddlware
processed first, because it should catch as
many errors as possible.
This can be archived by adding it last in the stack because that means it will be the first one called
on a request due to the LIFO
order of execution.
Everything that happens before the process
function of ExceptionHandlingMiddlware
will not be caught
and handled by the custom error handler. In that case it falls back to the default error handler
from the web server.
File: vendor/samuelgfeller/slim-error-renderer/src/Middleware/ExceptionHandlingMiddleware.php
// ...
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (Throwable $exception) {
return $this->handleException($request, $exception);
}
}
// ...
The handleException
function is the custom error handler that logs the error and
renders the error page.
If the $request
contains the header Accept: application/json
, the error details are returned as JSON.
Else, the error is
rendered in an HTML page.
When the configuration value display_error_details
is true
, the error details
(exact error message and stack trace) are rendered in an
HTML page
or included in the JSON response.
If $displayErrorDetails
is false
, a
generic error page is displayed
or added to the JSON response containing only the status code
and reason phrase (e.g. 500 Internal Error or 404 Not found).
Exception heavy middleware
During development, notices and warnings should be addressed the same way as fatal errors.
The script should stop the execution, the error logged and an error details page with stack
trace displayed in the browser.
Symfony and Laravel are "exception-heavy" in debug mode for a long time already.
To archive this, notices and warnings are transformed into exceptions that can be caught by the error handling middleware.
The library slim-error-renderer
provides the
NonFatalErrorHandlingMiddlware
which throws an ErrorException
if display_error_details
is true
.
This middleware is also in charge of logging the non-fatal errors, as in production (when
display_error_details
is false
),
no exception is thrown and the error handling middleware is not called and cannot log the error.
The PHP function set_error_handler
enables the custom handling of the non-fatal errors.
To enable the exception heavy feature, the NonFatalErrorHandlingMiddlware
just has to be
instantiated with the configuration
and then added to the middleware stack.