Pragmatism in the real world

Custom error rendering in Slim 4

One of the nice things about Slim 4 is that it’s easier to customise the HTML generated on error without having to worry about the rest of the error handling mechanism. This is because we have separated error rendering from error handling.

Slim’s default ErrorHandler maintains a list of renderers, one for each content-type and will delegate the creation of the error payload (HTML, JSON, XML, etc) to the renderer. Out of the Box, the error hander registers an HtmlErrorRenderer which provides a very basic error display:

Slim4 default error no details

We can also enable the displayErrorDetails setting to view the information about the error:

Slim4 default error with details

Separately, the error handler can log these details to the error log.

Writing a custom HTML renderer

To write a new error renderer, we need to create a classs that implements SLim\Interfaces\ErrorRendererInterface. This interface requires a single method:

public function __invoke(Throwable $exception, bool $displayErrorDetails): string;

We are given the exception that caused this error and have to return the HTML we wish to be displayed.

Let’s create our own HTML error renderer. If you want to play along, my Slim4-Starter project is a good place to start from.

We start with a basic HtmlErrorRenderer, which we’ll store in the src/Error/Renderer directory:

<?php

declare(strict_types=1);

namespace App\Error\Renderer;

use Slim\Exception\HttpNotFoundException;
use Slim\Interfaces\ErrorRendererInterface;
use Throwable;

final class HtmlErrorRenderer implements ErrorRendererInterface
{
    public function __invoke(Throwable $exception, bool $displayErrorDetails): string
    {
        $title = 'Error';
        $message = 'An error has occurred.';

        if ($exception instanceof HttpNotFoundException) {
            $title = 'Page not found';
            $message = 'This page could not be found.';
        }

        return $this->renderHtmlPage($title, $message);
    }

    public function renderHtmlPage(string $title = '', string $message = ''): string
    {
        $title = htmlentities($title, ENT_COMPAT|ENT_HTML5, 'utf-8');
        $message = htmlentities($message, ENT_COMPAT|ENT_HTML5, 'utf-8');

        return <<<EOT
<!DOCTYPE html>
<html>
<head>
  <title>$title - My website</title>
  <link rel="stylesheet"
     href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.css">
</head>
<body>
  <h1>$title</h1>
  <p>$message</p>
</body>
</html>
EOT;
    }
}

We can do whatever we like in __invoke(). In this case, I have chosen to change the title and message based on the class of the $exception. Slim will show a HttpNotFoundException when the router cannot find a registered route for the request URL, so we can use this to show different text explaining the problem.

We also need to render the HTML itself. This is likely to match the rest of your website’s design, so the implementation of renderHtmlPage() is entirely up to you. I’m sure you can do better than I can though!

Register our renderer

We register our renderer, at the point we add the ErrorMiddleare, which in my case is in config/middleware.php:

    $displayErrorDetails = (bool)($_ENV['DEBUG'] ?? false);
    $errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, true, true);
    $errorHandler = $errorMiddleware->getDefaultErrorHandler();
    $errorHandler->registerErrorRenderer('text/html', HtmlErrorRenderer::class);

Don’t forget to add use App\Error\Renderer\HtmlErrorRenderer; at the top of the file and now the new error renderer will now be used.

This is what our new error page looks like:

Slim4 new error page

Using Whoops for development

When we’re developing, it would be useful to have more information about what’s gone wrong. The whoops error handler can provide this, so let’s add it. Fortunately for us, the hard work has been done already by Zeusis in their zeuxisoo/slim-whoops package, so just install it:

$ composer require "zeuxisoo/slim-whoops"

We can now add the WhoopsMiddleare if we are in debug mode:

    if ((bool)($ENV_['DEBUG'] ?? false)) {
        $app->add(new WhoopsMiddleware(['enable' => true]));
    } else {
        $errorMiddleware = $app->addErrorMiddleware(false, true, true);
        $errorHandler    = $errorMiddleware->getDefaultErrorHandler();
        $errorHandler->registerErrorRenderer('text/html', HtmlErrorRenderer::class);
    }

I’ve triggered it on the DEBUG environment variable, bu too course, you should use whatever method works for you in order to determine when the app is in development mode.

Slim4 whoops error

To sum up

That’s it. Changing the error rendering ensure that your error pages look part of your website and it’s easy to do so. In addition, the technique can also be used to render custom error payloads for APIs by registering error renderers for the error handler’s application/json and application/xml content types.

15 thoughts on “Custom error rendering in Slim 4

  1. Great post.
    You know how to handle error 500, try calling \ HttpInternalServerErrorException but it doesn't work for me

    use Slim\Exception\HttpInternalServerErrorException;
    //
    if ($exception instanceof HttpInternalServerErrorException) {
    $title = "500 that's an error";
    $message = 'there was an error trying to send your message. please try again later';
    //
    }

  2. Good afternoon.
    I am happy to follow and read your blog.
    I follow the news and the development of Slim.

    I worked with Slim 3 for a long time and I really like it.
    With the release of Slim 4, for me at least, I can hardly get used to the new approach. For some reason, it turned out to be difficult for me. But this is a special case. I am sure that for the majority this is a pleasant update.

    And Question .. :)
    In your Slim4-Starter, the HomePageHandler.php controller.
    I don’t understand how LoggerInterface gets into the constructor.

    Is this autowiring possible?

  3. Thank you for your tutorial! I have a question regarding the Error Log. I would like to use Monolog for Error Handler. Could you please give more instruction?

    Thank you

    1. +1. How to use monolog to write to a log inside a custom error handler? Cannot use dependency injection from a custom error handler…

  4. ErrorRendererInterface is a badly designed and pretty much dead and useless class for anything non-trivial. The example where you put in raw HTML code in HEREDOC (!!!) perfectly exemplifies this. I'm sorry but that's the fact.

    The signature just kills me:
    public function __invoke(Throwable $exception, bool $displayErrorDetails): string

    Why doesn't it receive ServerRequestInterface and return ResponseInterface, but string instead? I mean, what the hell, why put something like that in production? My use case was pretty common – render an error using Twig. This is impossible using this abstraction. I had to work around it using ->setDefaultErrorHandler(…) which – surprise surprise – actually has the function signature you would expect it to have, albeit it doesn't receive $displayErrorDetails or have a MIME type parser, creating the need to re-implement it.

    Please fix this.

    Correct signature should be:
    public function __invoke(ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails): ResponseInterface

    1. Slim's error handling is optional. You can always write and register your own error handling middleware that works the way you want it to.

      1. Rob, I understand, I'm not blaming you because I know exactly the black sheep, the imbecile in the core team who is responsible for this (and frankly many other ridiculous design ideas in Slim 4).

        I just want to know the reasoning behind this abstraction. Please tell me the use case in which this function would be appropriate, because I cannot think of any, and GitHub search for it shows that people are using it in a similarly castrated way as shown in your example.

    2. I fully agree, its an absolute convoluted spaghetti mess – the above example is simple, but look at Daniel Opitz skeleton example if you want to see an even more verbose complex way of doing this (no offence to Daniel), but I know his implementation was the only way to accomplish this… in a nutshell error handling in Slim 4 is poorly designed.

  5. I second Alex's comment: How to inject container in this class?

    How can data in the DI container be injected into the Error Renderer. Alternatively, how can the Error Renderer get data from the container.

    I want to include application configuration from the container in the error document generated by the Error Renderer.

  6. Adding in error handeling into my skeleton at the moment.

    To everyone else who has some issues with this, check out odan's answer on this. https://stackoverflow.com/questions/62178065/custom-error-handling-without-try-except-in-php-slim-framework

    Since it's just normal middleware that wraps the handle() in a try catch you should be able to handle the errors anyway you want to, and inject the Di containers in as per normal. (You could setup a long list of catch blocks for each error message to be handled differently for each type of error etc)

    Make sure the error middleware is where you expect it to be in the onion tho.

    The built in slim error handler middleware thing confused me a bit to.

Comments are closed.