How Symfony2 turns exceptions into error pages and how to customize those

This article explains how the Symfony2 kernel deals with exceptions and creates error pages for them. You will also learn how to efficiently develop and preview custom error pages.

Updated on March 19, 2014

Great news! The Symfony2 cookbook page has been updated and now mentions the bundle described here. We've updated this article accordingly.

There's a recipe over in the Symfony2 cookbook how to change the default error pages displayed when an uncaught exception occurs in your Symfony2 application.

What it does not tell you is how to efficiently develop and preview such custom error pages. But before we come to that, let me explain what happens behind the scenes when an exception occurs.

Control flow during exception handling

0. All requests for your Symfony2 application go through the HttpKernel's handle() method. Whenever an exception occurs that is not contained within the application but reaches the HttpKernel level, …

1. ... the kernel dispatches a KernelEvents::EXCEPTION type event to notify interested listeners of the situation. Listeners can either change the exception or provide a Response object. If a response is provided, it basically gets returned from the initial handle() method. If not, the exception is thrown out of the kernel.

That's it from the kernel's perspective already. So let's have a look at the event listeners that do the heavy lifting.

2. The HttpKernel component also contains the ExceptionListener class that does two things: First, it logs the exception using the LoggerInterface. Second, it dispatches a sub-request back to our distressed kernel. This sub-request is set up to run a controller of a given (configurable) name and receives the exception (actually, a FlattenException instance of it) as a controller parameter. See the "kernel.exception in the Symfony Framework" box over here for details.

3. The actual setup as an event subscriber happens in the TwigBundle where a service twig.exception_listener is defined. This service uses the ExeptionListener class, tweakable through the %twig.exception_listener.class% parameter. Remember that this listener needs the name of a controller to run? It is given by yet another parameter:

4. The %twig.exception_listener.controller% parameter sets the controller to dispatch for exceptions. This parameter is set by the TwigExtension and can be changed through the exception_controller configuration setting mentioned in the TwigBundle configuration reference. The default value of "twig.controller.exception:showAction" results from TwigBundle's Configuration class.

Wait – twig.controller.exception:showAction? What's that?

This particular syntax refers to a controller defined as a service and means the showAction() method on a service named twig.controller.exception.

5. The service definition for this controller by default creates an instance of the ExceptionController which is part of the TwigBundle. Again, the particular class to be used can be changed by modifying the %twig.controller.exception.class% parameter. This controller gets the Twig rendering service as well as the %kernel.debug% flag constructor-injected.

Now that took a lot of dependency injection magic and quadruple-dispatch to get here, right? When you're still with me, relax. We'll see some real code now by heading over to …

6. … the ExceptionController, which is pretty straightforward. It determines the name of a template to render and uses Twig to create the Response from it. The template is passed the FlattenException instance ("exception") as well as "status_code" and "status_text" variables that contain the HTTP status code and descriptions.

The algorithm used to determine the name of the template is described in the cookbook. You can use the default way of overriding bundle templates to provide your own templates for this logical name.

Exception versus error pages

The ExceptionController receives the %kernel.debug% flag to determine whether to display exception (debug mode) or error (non-debug mode) pages. The corresponding template files start with "exception…" or "error…" respectively.

You are probably familiar with the default exception pages – the grey-looking ones providing helpful messages, stack traces and much more during development. Usually you don't want or need to change those.

The default error pages, however, just provide a short "Oops! An error occurred"-like text. Being a helpful netizen, this is probably not what you want your users to see. 

The problem when writing your own error pages

The default ExceptionController has a little shortcoming when it comes to crafting well-designed and helpful error pages: In order to make it render your error(xxx).twig files, you need to put it in non-debug mode (and throw the necessary exception, obviously).

But setting kernel.debug to false also stops Symfony from checking your config file for changes, by default will disable Assetic's controller used to generate assets on-the-fly and most importantly will also stop Twig from re-compiling changed template files.

You could manually clear the kernel cache after every change, but that's not a very efficient style of development.

Meet webfactory/exceptions-bundle

Now here's how WebfactoryExceptionsBundle comes into play: It contains a controller particularly designed for triggering the error pages.

Just add the following to your routing_dev.yml file:

    resource: "@WebfactoryExceptionsBundle/Resources/config/routing.yml"

This will set up a route at /_error/{statuscode}/{format} which you can use to throw an exception and get the corresponding error page. The format is optional, so /_error/404 will give you the the "page not found" error page.

Then, just follow the way outlined in the cookbook to override or customize the default templates.

In fact, the bundle just replaces TwigBundle's default ExceptionController with a particular subclass that allows to change the "debug" setting at a later time. When you're calling the TestController, it will simply look up the ExceptionController in the DI container (remember, it is available as a service!), set the "debug" flag to false and throw an exception to set things off.

Besides that, WebfactoryExceptionsBundle also provides some building blocks for more helpful error pages, but I'll cover that in another posting.

Was this helpful for you? Let @webfactory or @mpdude_de know!