Creating a Custom Statamic 500 Server Error Page: Part Two

September 12, 2021 —John Koster

This is a follow up article to the Creating a Custom Statamic 500 Server Error Page article. In the previous article, we created a generic server error page that would display the same information regardless of what type of error was raised within the site's code base. In this article, we will create a new server error page with the added benefit that we will be able to change how we render our error page based on what kind of error was thrown.

Statamic 3 leans heavily on the Laravel framework for a lot of the client/server interaction, and this is also true for the handling of error responses. Laravel provides extensive options and methods for customizing the reporting and rendering of exceptions, all of which are powerful in their own right. However, we will make use of a few different features to create something that will allow us to easily write a dynamic Antlers page depending on what exception was thrown:

  • Customization of the app/Exceptions/Handler.php exception handler
  • Creation of a simple view composer to supply additional error data to Antlers

#Creating the Error Template File

To get started, we will create a new file within the resources/views/errors directory named 500.antlers.html. The .antlers.html extension is important, and will allow us to utilize Antlers when writing our custom error page. We will set the contents of this file to the following for now:

1{{ dump }}

Our project's directory structure should now look similar to the following:

1errors/
2 404.antlers.html
3 500.antlers.html
4default.antlers.html
5home.antlers.html
6layout.antlers.html

#Creating a Custom Tag to Help Test Our Error Page

Like in the previous article, we will create a custom Antlers tag that will throw an exception whenever it is encountered. We will use this tag to be able to easily test our custom error page later.

Statamic's please command line utility will be utilized to scaffold the necessary tag file. The please utility contains a command called make:tag that will create the tag's PHP file for us within our project's app/Tags/ directory (the Tags directory will be created if it does not already exist). The command accepts the name of our new tag; in the following example ServerError is the name of our new Antlers tag (make sure to run this command from project's root directory, which should contain the please file):

1php please make:tag ServerError

A new file should now be located at app/Tags/ServerError.php with the following contents:

1<?php
2 
3namespace App\Tags;
4 
5use Statamic\Tags\Tags;
6 
7class ServerError extends Tags
8{
9 /**
10 * The {{ server_error }} tag.
11 *
12 * @return string|array
13 */
14 public function index()
15 {
16 //
17 }
18 
19 /**
20 * The {{ server_error:example }} tag.
21 *
22 * @return string|array
23 */
24 public function example()
25 {
26 //
27 }
28}

Statamic will automatically load the tag classes from this directory for us, so there are no extra steps involved to actually hook it up to the Antlers template engine. Each public method within a Statamic tag class can be accessed from a template using the <TAG_NAME>:<METHOD_NAME> syntax, and the index method can be used by just supplying the tag's name. For our new tag, we will only be using the index method so we can write something like this in our template:

1{{ server_error }}

In our template we use server_error instead of ServerError - all CamelCased names must be converted to snake_cased names when they are used in our Antlers template. We now need to add some code to our index method that will trigger an error allowing us to test our new error page. This can be done by simply throwing an Exception:

1<?php
2 
3namespace App\Tags;
4 
5use Exception;
6use Statamic\Tags\Tags;
7 
8class ServerError extends Tags
9{
10 /**
11 * The {{ server_error }} tag.
12 *
13 * @return string|array
14 * @throws Exception
15 */
16 public function index()
17 {
18 throw new Exception('This is a test exception.');
19 }
20 
21}

After this change, whenever the Antlers templating engine encounters the {{ server_error }} tag a new Exception will be thrown, allowing us to test our error page whenever we want. While our new tag works, the {{ server_error }} can be shortened to something more convenient by making use of a tag alias.

Statamic allows tag authors to provide different names for their tags through the use of the alias feature. We will take advantage of this feature to give our tag a simpler name of {{ 500 }}. In doing so, our tag will also feel more natural when comparing it to the built-in {{ 404 }} tag. To add a tag alias, we simply have to define a static $aliases property with a list of all the tag's aliases:

1<?php
2 
3namespace App\Tags;
4 
5use Exception;
6use Statamic\Tags\Tags;
7 
8class ServerError extends Tags
9{
10 // Anything that appears in this list can be used
11 // in our templates in place of server_error.
12 protected static $aliases = ['500'];
13 
14 /**
15 * The {{ server_error }} tag.
16 *
17 * @return string|array
18 * @throws Exception
19 */
20 public function index()
21 {
22 throw new Exception('This is a test exception.');
23 }
24 
25}

After adding this, we can now use the {{ 500 }} tag in our Antlers templates to trigger a server error whenever we want. Go ahead and add this to your site's /resources/views/layout.antlers.html file so that it will throw an error each time we load a page for testing.

#Preparing Our Local Project

To have our custom error page show up we need to temporarily update our .env file and adjust the value of the APP_DEBUG entry to false:

1...
2APP_DEBUG=false
3...

If your custom error page still does not appear, it is likely that your project's configuration values have been cached. To solve this issue, simply run the following command line utility from the root directory of your project (where the artisan file is located):

1php artisan config:clear

After this command has finished your custom error page should now appear once the page is refreshed. If your custom error page still does not appear, the next most common reason is that the config/app.php configuration file has been modified to not load the configuration value from the .env file.

To resolve this issue, open the config/app.php file in your editor and locate the debug entry:

1<?php
2 
3return [
4 
5 // ...
6 
7 /*
8 |--------------------------------------------------------------------------
9 | Application Debug Mode
10 |--------------------------------------------------------------------------
11 |
12 | When your application is in debug mode, detailed error messages with
13 | stack traces will be shown on every error that occurs within your
14 | application. If disabled, a simple generic error page is shown.
15 |
16 */
17 
18 'debug' => true,
19 
20 // ...
21 
22];

Adjust the value of your debug entry to false and then clear the application's configuration cache by using the config:clear command mentioned earlier.

After getting our local project setup and configured to test the error page, we should now see something similar to the following in the web browser:

Our custom server error page

As you can see, we have access to an exception variable that we can use to retrieve the exception message, as well as other useful information. However, you may notice that our exception was wrapped in an instance of HttpException, making it difficult to determine what the original error was from within an Antlers template.

#Adjusting the Application Error Handler

In this section, we are going to make adjustments to our project's error handler located within the app/Exceptions/Handler.php file. These changes are going to allow us to store a reference to any exception that is thrown so that we can access it later to provide additional details to our error template.

The main Laravel feature we will utilize is exception reporting, which allows us to register a callback that will be invoked whenever the error handler receives an exception. We will utilize this to set a static variable that will maintain a reference to the last exception thrown:

1<?php
2 
3namespace App\Exceptions;
4 
5use Throwable;
6use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
7 
8class Handler extends ExceptionHandler
9{
10 /**
11 * Maintains a reference to the last known throwable instance.
12 *
13 * @var Throwable|null
14 */
15 public static $lastThrowable = null;
16 
17 /**
18 * A list of the exception types that are not reported.
19 *
20 * @var array
21 */
22 protected $dontReport = [
23 //
24 ];
25 
26 /**
27 * A list of the inputs that are never flashed for validation exceptions.
28 *
29 * @var array
30 */
31 protected $dontFlash = [
32 'password',
33 'password_confirmation',
34 ];
35 
36 /**
37 * Register the exception handling callbacks for the application.
38 *
39 * @return void
40 */
41 public function register()
42 {
43 $this->reportable(function (Throwable $t) {
44 $previous = $t->getPrevious();
45 
46 if ($previous != null) {
47 self::$lastThrowable = $previous;
48 } else {
49 self::$lastThrowable = $t;
50 }
51 });
52 }
53 
54}

We are going to utilize a static variable so that we don't have to worry about multiple handler instances, singletons, or other things that will just get in our way to "easily" send this data to our Antlers error template later. Within the register method, we are also checking if the incoming error instance has a $previous instance by checking the results of the getPrevious() method.

We are doing this so that if a custom exception was wrapped inside another framework exception, we can get the original back out. Feel free to adjust the logic to whatever best suites your specific project.

#Creating a Custom View Composer

Now that we've modified our site's error handler, we will create a custom view composer that will allow us to provide additional details to our site's error template. To do this, we will simply update our site's application service provider located at app/Providers/AppServiceProvider.php. Within our service provider, we will register a new view composer using the view()->composer() helper function, and supply a callback that will get executed before our template is rendered.

We can get access to the view that Laravel is creating through the $view parameter, and it is by interacting with this view object that we can inject additional data to the template:

1<?php
2 
3namespace App\Providers;
4 
5use App\Exceptions\Handler;
6use Illuminate\Support\ServiceProvider;
7use Statamic\Statamic;
8 
9class AppServiceProvider extends ServiceProvider
10{
11 /**
12 * Register any application services.
13 *
14 * @return void
15 */
16 public function register()
17 {
18 //
19 }
20 
21 /**
22 * Bootstrap any application services.
23 *
24 * @return void
25 */
26 public function boot()
27 {
28 view()->composer('errors::500', function ($view) {
29 $exceptionType = '';
30 
31 if (Handler::$lastThrowable != null) {
32 $exceptionType = get_class(Handler::$lastThrowable);
33 }
34 
35 $view->with([
36 'original_exception' => Handler::$lastThrowable,
37 'exception_type' => $exceptionType
38 ]);
39 });
40 }
41}

You will notice in the previous code example that we are adding a new template variable named original_exception that is set to the static $lastThrowable variable we created in the previous section. Additionally, we are using PHP's get_class function to add a string variable containing the class name of the error. If we were to refresh the sample error page we created earlier, the results should now look similar to the following:

Customized Template Data

The reason why we've added the exception_type variable is that working with simple data types such as strings and arrays is much easier in Antlers compared to working with complex objects directly.

#Creating a Custom Exception Class

We are going to create a custom exception class just to observe how our template variables change depending on the type of error that was thrown. To do this, create a new PHP file at app/CustomException.php with the following contents:

1<?php
2 
3namespace App;
4 
5class CustomException extends \Exception { }

Now that we've created a custom exception class, let's update the Antlers tag we created earlier at app/Tags/ServerError.php to now have the following content:

1<?php
2 
3namespace App\Tags;
4 
5use Statamic\Tags\Tags;
6 
7class ServerError extends Tags
8{
9 protected static $aliases = ['500'];
10 
11 public function index()
12 {
13 throw new \App\CustomException('This is a test exception.');
14 }
15 
16}

Refreshing our local project should now display results similar to the following:

Example of Custom Exception Names

As you can see, our exception_type variable has been updated to refer to our custom App\CustomException class. It is this variable that we can make use of in our Antlers template to customize how our error page is displayed to our site's visitors.

#Creating a Dynamic Antlers Error Page

Now that we've updated our site's error handler and have created a custom view composer, it is now time to write some Antlers code that will change what message is displayed to the visitor depending on the type of error that was thrown. Let's update our errors/500.antlers.html file with the following content:

1<h1>Something bad happened!</h1>
2 
3{{ if exception_type == 'App\CustomException' }}
4 <h2>Our custom exception was thrown!</h2>
5{{ elseif exception_type == 'Exception' }}
6 <h2>Something else went wrong.</h2>
7{{ /if }}

Refreshing our page should now produce results similar to the following:

Custom Exception Message Preview

If we go back and update our app/Tags/ServerError.php file to throw an instance of \Exception instead of our custom \App\CustomException class, we should see our template react accordingly:

Generic Exception Message Preview

#Wrapping Up

In this article we made two relatively simply changes to our site (updating the error handler, and registering a custom view composer) that allow us to build a dynamic error page. It is possible to build upon these changes to create a robust server error template for your Statamic site, or adapt them in interesting ways to customize your site's template data.

Some absolutely amazing
people

The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.