Andrew Welch

Andrew Welch · Insights · #craftcms #frontend #errors

Making the web better one site at a time, with a focus on performance, usability & SEO

Published , updated · 5 min read ·


For more tools, technologies, and techniques, check out the devMode.fm podcast!

Handling Errors Gracefully in Craft CMS

Every website can have errors, it’s how you handle them that matters. Here’s a practical error handling guide for Craft CMS.

Oh No Craft Error Monkey

We’ve all likely heard the epigram:

Happiness isn’t the lack of problems, it’s knowing how to deal with them.

The same applies to web development. Your website will inevitably encounter error states you didn’t expect, handling them gracefully is what matters.

This simply requires adopting some Defensive Programming techniques. While it’s a small amount of additional work to code defensively, once you start doing it, it’ll just become second nature.

And believe me, that extra up-front time will pay off in spades in terms of time spent debugging or patching your work when sh*t hits the fan.

Link What it means to code defensively

The first thing you need to do is accept the pessimistic attitude espoused by Murhpy’s law:

Anything that can go wrong will go wrong

While this may be an overly paranoid way to live life, it’s a perfectly reasonable—even mandatory—way to think about things when programming.

Murphys Law Errors Happen

What this means from a practical point of view is:

  • Write your code in a modular, reusable way
  • If any method you call returns an error code, check it
  • Never assume that a method returns actual data; check for null
  • Think of every conceivable way your client could mess things up
  • Handle these error states gracefully, in a user-friendly way

While this may seem basic, I see a surprising amount of code that blithely assumes everything just works. Take the opposite approach; assume failure.

What happens then is when an error state does happen, the website falls over, and you get panicked calls from the client in the wee hours.

Link Defensive Coding in Twig

This mentality of defensive coding applies no matter what language you’re using. Whether it’s C, PHP, JavaScript, or even templating languages like Twig.

A good way to write code is to adopt coding standards; this ensures that you’re always doing things in a consistent way. This becomes more important when you’re working with a team, but future-you will also benefit from consistent, standardized coding.

You can adopt the Sensio Labs Twig Coding Standards, or you can adopt your own.

It’s less important what coding standards you adopt than it is that you’re using some type of standard.

Now that you’re on-board adopting some form of coding standards, let’s talk about things you can do while you’re developing a Craft CMS website.

The very first thing you absolutely must be doing is have devMode ON for local development. Having devMode on will catch all sorts of soft errors that might not be apparent if you don’t have it on.

The Multi-Environment Config for Craft CMS article shows how you can have devMode (amongst other things) on or off depending on the environment.

With devMode on, and all of our soft errors fixed, let’s have a look at coding defensively in Twig. A very common pattern when using Twig with Craft is doing something like this:

{% set image = entry.someImage.first() %}
<img src="{{ image.getUrl() }}" />

What could possibly go wrong? Well, a lot!

  • What if the entry doesn’t exist?
  • What if the client never added an Asset to the someImage field?
  • What if we didn’t restrict the client to uploading only images, and they added an Adobe Illustrator file?

These are the types of things you should be thinking about when you’re in mid-keystroke.

What I’ve found to be the most effective for handling this type of common pattern is to use the Twig ?? null coalescing operator. It looks something like this:

{% set image = entry.someImage.first() ?? someGlobal.defaultImage.first() ?? null %}
<img src="{{ image.getUrl() }}" />

What this does is set the image to the first expression that is defined and not null. So it’ll use entry.someImage.first() if that is defined/not null, or someGlobal.defaultImage if that is defined/not null, and finally just return null if nothing else matched.

It handles checking each object in the “dot notation” syntax, so for example it will make sure that entry, someImage, and the result of first() are all defined and not null.

In this case, someGlobal is a Craft CMS Global, in which we can put a default image if one hasn’t been filled in. While this isn’t required, it can be nice in some circumstances, and shows how the ?? null coalescing operator can be passed any number of fallbacks.

This is much nicer than doing something like:

{% if entry is defined and entry |length and entry.someImage is defined and entry.someImage | length %}

You can use the null coalescing operator for anything, from Assets to titles, whatever. It’s a nice way to do all of the requisite defined/not null checks, and provide fallbacks and defaults. For example:

{% set thisTitle = entry.title ?? category.title ?? someGlobal.title ?? 'Some Default Title' %}

The null coalescing operator is built into Twig as of Twig version 1.24 (January 25th, 2016), which is available in Craft as of Craft 2.6.771 (March 8th, 2016).

If you want to know more about the null coalescing operator, check out the Twig’s null-coalescing operator (??)! Straight Up Craft Hangout.

Observant readers will note that we could still end up with a null value for image here; so let’s address that too:

{% set image = entry.someImage.first() ?? someGlobal.defaultImage.first() ?? null %}
{% if image and image.kind == "image" %}
    <img src="{{ image.getUrl() }}" />
{% endif %}

That looks a lot better. We’re making sure that the image is not null, and we make sure that the image.kind an actual image file type. However, we should also remember to restrict the type of Assets that the client can upload to be just images as well.

I’ll even go one step further, and state that it’s a mistake to ever be outputting any image that the client has uploaded. As stated in the Creating Optimized Images in Craft CMS article, we should be transforming and optimizing any images displayed on the frontend.

Link Handling Exceptions on the Frontend

So what about other errors that can happen on the frontend, like web server or database errors? In Craft, these are known as exceptions. They aren’t supposed to happen, so they are exceptional.

Server Fire Handling Exceptions

The most well-known exception is a 404 error, which happens when there is a request for a file that doesn’t exist on the web server. In these cases, we want to handle it gracefully, and display a nice friendly error.

This is good for SEO reasons, and it’s also just plain friendly to the people who are visiting your website. If there is a 404 error on the nystudio107.com website, for instance, we encourage people to stick around:

Nystudio107 404 Error

nystudio107.com’s 404.html template

The way this works in Craft is that is looks for a template named after the http status code, in this case the name would be 404.html or 404.twig.

By default it looks for these in the root of your templates folder, but you can put them anywhere you want using the errorTemplatePrefix config setting.

The interesting thing here is that this works for any http status code. Just name the template after the http status code, and away you go.

Why might you want to do this? Well, the default Craft error handler will display an error code and an error message. If the worst happens, you might want to put a friendlier face on it for your clients than just something like:

Craft Cdbexception Error

Additionally, some penetration tests will flag this as a security issue, because it’s giving information about the type of database used, the nature of the error, etc.

It’d be much better for everyone concerned if we just had a nice friendly error message, such as the one Twitter was famous for:

Twitter Fail Whale

Twitter’s humorous Fail Whale graphic

We might not want to create a separate template for every possible http status code, though, so let’s dive a bit deeper. This is exactly what Craft does when it looks for a template to render when an error happens:

	/**
	 * Renders an error template.
	 *
	 * @throws \Exception
	 * @return null
	 */
	public function actionRenderError()
	{
		$error = craft()->errorHandler->getError();
		$code = (string) $error['code'];

		if (craft()->request->isSiteRequest())
		{
			$prefix = craft()->config->get('errorTemplatePrefix');

			if (craft()->templates->doesTemplateExist($prefix.$code))
			{
				$template = $prefix.$code;
			}
			else if ($code == 503 && craft()->templates->doesTemplateExist($prefix.'offline'))
			{
				$template = $prefix.'offline';
			}
			else if (craft()->templates->doesTemplateExist($prefix.'error'))
			{
				$template = $prefix.'error';
			}
		}

So it first looks for a template that matches the exact http status code. If that’s not found, then if it’s a 503 service not available error, it looks for a template named offline (more on that later), and finally it falls back on just looking for a template named error.

So perfect! We can create error handling templates for specific error codes that we want to handle in a special way, like 404 errors, and then we can have a generic catch-all error.html or error.twig template!

Now we have an easy way to gracefully handle any errors that happen in a user-friendly way, and we can control what error information is ever displayed publicly. It’s not as cool as the Fail Whale, but it’s better than a scary CDbException error:

Nystudio107 Generic Error

nystudio107.com’s error.html template

N.B.: If you have devMode on (which you should always have on when developing in local dev), all Craft exceptions will be displayed using the Craft debug template. That means they will not be routed to your custom error pages. To test them, either turn devMode off, or just navigate directly to the template in question.

Link Offline Staging Sites

Earlier we discussed that a 503 service unavailable error will result in Craft special-casing for an offline.html or offline.twig template.

We can take advantage of this behavior to make sure our client staging sites are not only not being index by GoogleBot and other such crawlers, but also that only our client can see the website as we work on it.

In the Preventing Google from Indexing Staging Sites article, we discussed using robots.txt and a multi-environment setup to do this. Here’s another way to do it that you may find even more convenient.

Craft Cms Is System On

Craft’s isSystemOn config setting

Craft has a config setting called isSystemOn that we can set to false for staging or other environments that we don’t want anyone to access. This is a multi-environment config variable, that we might set something like this:

<?php

/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here.
 * You can see a list of the default settings in craft/app/etc/config/defaults/general.php
 */

// $_ENV constants are loaded from .env.php via public/index.php
return array(

    // All environments
    '*' => array(
        'omitScriptNameInUrls' => true,
        'usePathInfo' => true,
        'cacheDuration' => false,
        'cacheMethod' => 'redis',
        'useEmailAsUsername' => true,
        'generateTransformsBeforePageLoad' => true,
        'requireMatchingUserAgentForSession' => false,
        'userSessionDuration' => 'P1W',
        'rememberedUserSessionDuration' => 'P4W',
        'siteUrl' => getenv('CRAFTENV_SITE_URL'),
        'craftEnv' => CRAFT_ENVIRONMENT,
        'backupDbOnUpdate' => false,
        'defaultSearchTermOptions' => array(
            'subLeft' => true,
            'subRight' => true,
        ),

        'defaultTemplateExtensions' => array('html', 'twig', 'rss'),

        // Set the environmental variables
        'environmentVariables' => array(
            'baseUrl' => getenv('CRAFTENV_BASE_URL'),
            'basePath' => getenv('CRAFTENV_BASE_PATH'),
            'staticAssetsVersion' => '106',
        ),
    ),

    // Live (production) environment
    'live'  => array(
        'isSystemOn' => true,
        'devMode' => false,
        'enableTemplateCaching' => true,
        'allowAutoUpdates' => false,
    ),

    // Staging (pre-production) environment
    'staging' => array(
        'isSystemOn' => false,
        'devMode' => false,
        'enableTemplateCaching' => true,
        'allowAutoUpdates' => false,
    ),

    // Local (development) environment
    'local' => array(
        'isSystemOn' => true,
        'devMode' => true,
        'enableTemplateCaching' => false,
        'allowAutoUpdates' => true,
        'disableDevmodeMinifying' => true,

        // Set the environmental variables
        'environmentVariables' => array(
            'baseUrl'  => getenv('CRAFTENV_BASE_URL'),
            'basePath' => getenv('CRAFTENV_BASE_PATH'),
            'staticAssetsVersion' => time(),
        ),
    ),
);

What this setting does is it causes Craft to return a 503 service unavailable error code for any frontend request. This then causes Craft to throw an exception, and display the offline template. This is great, because we can control what appears there.

So this stops GoogleBot, crawlers, and other prying eyes from seeing the website as we work on it, but what about our clients?

Craft Offline Access Permissions

Craft Offline access permissions

In the AdminCP, Settings → UsersUser Group lets you set access permissions for user groups. As long as your client has Access the site when the system is off permission, they can log in to the AdminCP, and then access the site with aplomb.

Here’s what our 503 template looks like (it can be either 503 or offline, either works):

Nystudio107 503 Error

nystudio107.com’s 503.html template

You could even have your offline page be a frontend login form for your clients, to make it even easier for them.

In your code, you can even do conditionals based on isSystemOn too:

{% if craft.config.isSystemOn %}
    Hey, we're online!
{% else %}
    Ut oh, we're offline…
{% endif %}

Link Wrapping Up

Since a theme of the article has been to use some well-known pearls of wisdom, I leave you with one more:

An ounce of prevention is worth a pound of cure

The time you spend coding defensively and bulletproofing your websites will pay off with happy clients, and less frustration for you as a developer.

Happy Baby Face

At the very least, future-you will be extremely grateful that past-you did such a good job… because it’s almost inevitable that you’ll be the one working on it in the future.

${ category } · ${ blog.postDate }

${ blog.title }

#${ tag.title }