Andrew Welch
Posted on February 19, 2020
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.
Andrew Welch / nystudio107
Update: This article has been updated to use Craft CMS 3.x syntax.
We’ve all likely heard the epigram:
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.
What it means to code defensively
The first thing you need to do is accept the pessimistic attitude espoused by Murhpy’s law:
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.
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.
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.
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.one() %}
<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.
Coalescing the Night Away
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.one() ?? someGlobal.defaultImage.one() ?? 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.one() 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 one() 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 %}
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.one() ?? someGlobal.defaultImage.one() ?? 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.
The null coalescing operator is a nice way to do all of the requisite defined/not null checks, and provide fallbacks and defaults. However, because a variable set to an empty string is defined and not null, this might not do what you’d expect:
{% set thisTitle = entry.title ?? category.title ?? global.title ?? 'Some Default Title' %}
The problem here is it’ll just pick the first thing that is defined and not null. So if entry.title is an empty string, it’ll use that, which is rarely what you want.
This is why I wrote the free Empty Coalesce plugin for Craft CMS 3. Empty Coalesce adds the ??? operator to Twig that will return the first thing that is defined, not null, and not empty. It’s that last bit that is key! So it becomes:
{% set thisTitle = entry.title ??? category.title ??? global.title ??? 'Some Default Title' %}
Now the first thing that is defined, not null, and not empty will be what thisTitle is set to.
Nice. Simple. Readable. And most importantly, likely the result you’re expecting.
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.
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:
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:
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:
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:
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.
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 has a config setting called isSystemLive (formerly 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(
'isSystemLive' => true,
'devMode' => false,
'enableTemplateCaching' => true,
'allowAutoUpdates' => false,
),
// Staging (pre-production) environment
'staging' => array(
'isSystemLive' => false,
'devMode' => false,
'enableTemplateCaching' => true,
'allowAutoUpdates' => false,
),
// Local (development) environment
'local' => array(
'isSystemLive' => 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?
In the AdminCP, Settings → Users → User 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):
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 isSystemLive too:
{% if craft.config.isSystemLive %}
Hey, we're online!
{% else %}
Ut oh, we're offline…
{% endif %}
Wrapping Up
Since a theme of the article has been to use some well-known pearls of wisdom, I leave you with one more:
The time you spend coding defensively and bulletproofing your websites will pay off with happy clients, and less frustration for you as a developer.
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.
Further Reading
If you want to be notified about new articles, follow nystudio107 on Twitter.
Copyright ©2020 nystudio107. Designed by nystudio107
Posted on February 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.