Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

How the HTML Error Page is Rendered

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When you use a browser, the format will be html. That's also the default format if no request format was set and if the request doesn't contain an Accept header. In the case of html, the serializer will fail by throwing a NotEncodableValueException. When that happens, this offloads the work to another error render: $this->fallbackErrorRenderer.

If you dumped this object, you'd find out that it's an instance of TwigErrorRenderer. Ok! Let's open that up: Shift + Shift TwigErrorRender.php.

It... interesting! It immediately calls another fallbackErrorRenderer. This one is an instance of HtmlErrorRender. Open it up: HtmlErrorRenderer.php.

Error Renderer Decoration

Then... stop. Let me explain why and how we have three different error renderer classes. This HtmlErrorRenderer is the, sort of, "core" error renderer and it always exists. But if you have Twig installed, the TwigErrorRenderer suddenly "takes over" the error rendering process. It does that via service decoration: TwigErrorRenderer decorates HtmlErrorRenderer.

And then... if you have the serializer component installed, suddenly there is a third error renderer added to the system: SerializerErrorRenderer, which decorates TwigErrorRenderer.

This is a slight over-simplification, but there is basically only ever one official "error renderer" service registered in the container. It's id is error_renderer. But through service decoration, multiple error renderers are ultimately used.

HtmlErrorRenderer: Default Exception & Error Templates

Let's look at the flow. TwigErrorRender calls render() on HtmlErrorRenderer. Remember: the render() method on all of these classes has the same job: to return a FlattenException that contains the status code, headers and the "body" that will be used for the Response.

So, it's no surprise that this once again starts by creating a FlattenException object. To get the "body" of the response, it calls $this->renderException(). Jump to that.

This is what builds the error or exception page. The $debugTemplate argument defaults to views/exception_full.html.php. Yea, this method render PHP templates! This template will be used in debug mode. If we're not in debug mode, then it "includes" - basically, renders - error.html.php. So exception_full.html.php in debug mode, error.html.php on production. The include() function is as simple as it gets.

Let's go see the debug template. Using the directory tree on top... click the error-handler/ directory, then navigate to open Resources/views/exception_full.html.php

This is what we're seeing in our browser right now. To prove it, in the middle, let's add:

I'm inside your exception page!

... lines 1 - 2
<html lang="en">
... lines 4 - 12
... lines 14 - 35
I'm inside your exception page!
... lines 37 - 41
... lines 44 - 45

Back on the browser, refresh the 404 page. There's our text! Go... take that out.

So this template and error.html.php are responsible for rendering the debug and production HTML error pages out-of-the-box.

Close exception_full.html.php... and also HtmlErrorRenderer.php.

TwigErrorRenderer: Twig Overrides

Back in TwigErrorRenderer, this starts by getting the FlattenException from HtmlErrorRenderer. So then... if we already have the finished FlattenException, what's the point of this class?

This entire class exists to give you - the application developer - the ability to override what the error template looks like. $this->findTemplate() is used to check if you have a Twig override template. If you don't, the FlattenException from HtmlErrorRenderer is used. But if you do have an override template, it renders that and uses its HTML.

Twig Namespaces & Override Templates

Scroll down to the findTemplate() method. Cool! It first looks for a template called @Twig/Exception/error%s.html.twig, where the %s part is the status code. The @Twig thing is a Twig namespace. Every bundle in your app automatically has one. Want to render a template from FooBarBundle? You could do that by saying @FooBar then the path to the template from within that bundle.

This is normally used as a way for a bundle to render a template inside itself. But Symfony also registers an override path for every bundle namespace. When you say @Twig/Exception/error404.html.twig, Twig first looks for the template at templates/bundles/TwigBundle/Exception/error404.html.twig.

Anyways, if this template exists because you created it, it will be used. Otherwise, it looks for a generic error.html.twig that handles all status codes. This is how the Twig error template overrides work.

And... phew! That's it! SerializerErrorRenderer renders XML & JSON pages, or, really, anything format that the serializer supports. HtmlErrorRenderer renders the HTML pages and TwigErrorRenderer allows you to override that with carefully-placed Twig templates.

Finishing the Process

Close both of the error renderers. We now know that there are many ways to hook into the exception-handling process. You can override ErrorController, listen to the kernel.exception event, customize the ProblemNormalizer for JSON or XML exceptions or add a Twig template override for custom HTML.

No matter what, ErrorListener sets this Response onto the ExceptionEvent. In HttpKernel, if the event has a response, there's a bit of final status code normalization, but it eventually passes the Response to filterResponse(). So yes, even an error page will trigger that event, which is why a 404 page has the web debug toolbar.

Ok team, we're now truly done walking through the HttpKernel process: both the happy and unhappy paths. Next, let's use our new knowledge... to start hacking into the system.

Leave a comment!

Login or Register to join the conversation

@Rayan, as not usual with your tutorials, this is so complicated!


Hey Houssem,

This is advanced topic, you already should have some knowledge about Symfony. And yes, internal code that stand behind all the magic in Symfony is be complex, but its main concepts should be simple for understanding. We do our best to explain it simple. What exactly is so complex for you? Were you able to understand it though it's complex, or you didn't understand this topic at all? Were you code along with Ryan when watching this course? Have you tried to re-watch the topic you didn't understand one more time?



Hey victor ^^!

A big thank you for your splendid answer.

As a Symfony developer I try to go behind the scenes (and contribute someday in Symfony). I found the 19 chapters quite theoretical and abstract but after that it is fine for me ^^. I think that I will take again the tutorial ^^



Hey Houssem,

Yeah, I probably see what you mean! Explaining every line would stretch tutorial time and also might be confuse due to a lot of information, so we're trying to keep the balance explaining internal things - probably not too thoroughly but enough for understanding the main concepts. And yes, it's not about explaining how make changes in core but more about how to read and understand the code, so it's more like theoretical explanation.

> and contribute someday in Symfony

This sounds awesome! Probably the existent code might be difficult to tweak as it was watched and polished by many developers. I'd recommend you to watch for issues in symfony/symfony repo where you can pick one and try to solve - it might be easier to start contributing this way. Also, sometimes issues has "easy pick" tags that means it should not be too much difficult task that you might be interested in.

As alternative - you can try to contribute to Symfony docs first - there're a lot of improvements that might be done there, or sometimes you can find some misprints that should be fixed as well. Or, even consider to add more examples to the parts of the docs that are missing them.

I hope this helps! ;)


1 Reply

Big tanks ^^!. I will do it ^^!


Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works well for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.3
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.12.1
        "doctrine/doctrine-bundle": "^2.0", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.2.2
        "doctrine/orm": "^2.5.11", // 2.8.2
        "easycorp/easy-log-handler": "^1.0", // v1.0.9
        "http-interop/http-factory-guzzle": "^1.0", // 1.0.0
        "knplabs/knp-markdown-bundle": "^1.7", // 1.9.0
        "knplabs/knp-paginator-bundle": "^5.0", // v5.4.2
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.1
        "knplabs/knp-time-bundle": "^1.8", // v1.16.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.24
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.9.1
        "liip/imagine-bundle": "^2.1", // 2.5.0
        "oneup/flysystem-bundle": "^3.0", // 3.7.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.2
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.1", // v5.6.1
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.9", // v1.17.5
        "symfony/form": "5.0.*", // v5.0.11
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/mailer": "5.0.*", // v5.0.11
        "symfony/messenger": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.5", // v3.6.0
        "symfony/property-access": "5.0.*|| 5.1.*", // v5.1.11
        "symfony/property-info": "5.0.*|| 5.1.*", // v5.1.10
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/security-bundle": "5.0.*", // v5.0.11
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.11
        "symfony/serializer": "5.0.*|| 5.1.*", // v5.1.10
        "symfony/twig-bundle": "5.0.*", // v5.0.11
        "symfony/validator": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.4", // v1.11.1
        "symfony/yaml": "5.0.*", // v5.0.11
        "twig/cssinliner-extra": "^2.12", // v2.14.3
        "twig/extensions": "^1.5", // v1.5.4
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.0
        "twig/inky-extra": "^2.12", // v2.14.3
        "twig/twig": "^2.12|^3.0" // v2.14.4
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.4.0
        "fakerphp/faker": "^1.13", // v1.13.0
        "symfony/browser-kit": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/maker-bundle": "^1.0", // v1.29.1
        "symfony/phpunit-bridge": "5.0.*", // v5.0.11
        "symfony/stopwatch": "^5.1", // v5.1.11
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/web-profiler-bundle": "^5.0" // v5.0.11