Handling Errors Gracefully in Craft CMS

gaijinity

Andrew Welch

Posted on February 19, 2020

Handling Errors Gracefully in Craft CMS

Handling Errors Gracefully in Craft CMS

Every web­site can have errors, it’s how you han­dle them that mat­ters. Here’s a prac­ti­cal error han­dling guide for Craft CMS.

Andrew Welch / nystudio107

Oh No Craft Error Monkey

Update: This arti­cle has been updat­ed to use Craft CMS 3.x syntax.

We’ve all like­ly heard the epigram:

The same applies to web devel­op­ment. Your web­site will inevitably encounter error states you did­n’t expect, han­dling them grace­ful­ly is what matters.

This sim­ply requires adopt­ing some Defen­sive Pro­gram­ming tech­niques. While it’s a small amount of addi­tion­al work to code defen­sive­ly, once you start doing it, it’ll just become sec­ond nature.

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

What it means to code defensively

The first thing you need to do is accept the pes­simistic atti­tude espoused by Murh­py’s law:

While this may be an over­ly para­noid way to live life, it’s a per­fect­ly rea­son­able — even manda­to­ry — way to think about things when programming.

Murphys Law Errors Happen

What this means from a prac­ti­cal point of view is:

  • Write your code in a mod­u­lar, reusable way
  • If any method you call returns an error code, check it
  • Nev­er assume that a method returns actu­al data; check for null
  • Think of every con­ceiv­able way your client could mess things up
  • Han­dle these error states grace­ful­ly, in a user-friend­ly way

While this may seem basic, I see a sur­pris­ing amount of code that blithe­ly assumes every­thing just works. Take the oppo­site approach; assume failure.

What hap­pens then is when an error state does hap­pen, the web­site falls over, and you get pan­icked calls from the client in the wee hours.

Defen­sive Cod­ing in Twig

This men­tal­i­ty of defen­sive cod­ing applies no mat­ter what lan­guage you’re using. Whether it’s C, PHP, JavaScript, or even tem­plat­ing lan­guages like Twig.

A good way to write code is to adopt cod­ing stan­dards; this ensures that you’re always doing things in a con­sis­tent way. This becomes more impor­tant when you’re work­ing with a team, but future-you will also ben­e­fit from con­sis­tent, stan­dard­ized coding.

You can adopt the Sen­sio Labs Twig Cod­ing Stan­dards, or you can adopt your own.

Now that you’re on-board adopt­ing some form of cod­ing stan­dards, let’s talk about things you can do while you’re devel­op­ing a Craft CMS website.

The very first thing you absolute­ly must be doing is have dev­Mode ON for local devel­op­ment. Hav­ing devMode on will catch all sorts of soft errors that might not be appar­ent if you don’t have it on.

The Mul­ti-Envi­ron­ment Con­fig for Craft CMS arti­cle shows how you can have devMode (amongst oth­er things) on or off depend­ing on the environment.

With devMode on, and all of our soft errors fixed, let’s have a look at cod­ing defen­sive­ly in Twig. A very com­mon pat­tern when using Twig with Craft is doing some­thing like this:


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

What could pos­si­bly go wrong? Well, a lot!

  • What if the entry does­n’t exist?
  • What if the client nev­er added an Asset to the someImage field?
  • What if we did­n’t restrict the client to upload­ing only images, and they added an Adobe Illus­tra­tor file?

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

Coa­lesc­ing the Night Away

What I’ve found to be the most effec­tive for han­dling this type of com­mon pat­tern is to use the Twig ?? null coa­lesc­ing oper­a­tor. It looks some­thing 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 expres­sion 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 final­ly just return null if noth­ing else matched.

It han­dles check­ing each object in the ​“dot nota­tion” syn­tax, so for exam­ple 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 Glob­al, in which we can put a default image if one has­n’t been filled in. While this isn’t required, it can be nice in some cir­cum­stances, and shows how the ?? null coa­lesc­ing oper­a­tor can be passed any num­ber of fallbacks.

This is much nicer than doing some­thing like:


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

The null coa­lesc­ing oper­a­tor is built into Twig as of Twig ver­sion 1.24 (Jan­u­ary 25th, 2016), which is avail­able in Craft as of Craft 2.6.771 (March 8th, 2016).

If you want to know more about the null coa­lesc­ing oper­a­tor, check out the Twig’s null-coa­lesc­ing oper­a­tor (??)! Straight Up Craft Hangout.

Obser­vant read­ers will note that we could still end up with a null val­ue 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 bet­ter. We’re mak­ing sure that the image is not null, and we make sure that the image.kind an actu­al image file type. How­ev­er, we should also remem­ber to restrict the type of Assets that the client can upload to be just images as well.

I’ll even go one step fur­ther, and state that it’s a mis­take to ever be out­putting any image that the client has uploaded. As stat­ed in the Cre­at­ing Opti­mized Images in Craft CMS arti­cle, we should be trans­form­ing and opti­miz­ing any images dis­played on the frontend.

The null coa­lesc­ing oper­a­tor is a nice way to do all of the req­ui­site defined/​not null checks, and pro­vide fall­backs and defaults. How­ev­er, because a vari­able set to an emp­ty 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 prob­lem here is it’ll just pick the first thing that is defined and not null. So if entry.title is an emp­ty string, it’ll use that, which is rarely what you want.

This is why I wrote the free Emp­ty Coa­lesce plu­g­in for Craft CMS 3. Emp­ty Coa­lesce adds the ??? oper­a­tor to Twig that will return the first thing that is defined, not null, and not emp­ty. 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 emp­ty will be what thisTitle is set to.

Nice. Sim­ple. Read­able. And most impor­tant­ly, like­ly the result you’re expecting.

Han­dling Excep­tions on the Frontend

So what about oth­er errors that can hap­pen on the fron­tend, like web serv­er or data­base errors? In Craft, these are known as excep­tions. They aren’t sup­posed to hap­pen, so they are excep­tion­al.

Server Fire Handling Exceptions

The most well-known excep­tion is a 404 error, which hap­pens when there is a request for a file that does­n’t exist on the web serv­er. In these cas­es, we want to han­dle it grace­ful­ly, and dis­play a nice friend­ly error.

This is good for SEO rea­sons, and it’s also just plain friend­ly to the peo­ple who are vis­it­ing your web­site. If there is a 404 error on the nys​tu​dio107​.com web­site, for instance, we encour­age peo­ple to stick around:

Nystudio107 404 Error

The way this works in Craft is that is looks for a tem­plate named after the http sta­tus code, in this case the name would be 404.html or 404.twig.

By default it looks for these in the root of your tem­plates fold­er, but you can put them any­where you want using the errorTem­platePre­fix con­fig setting.

The inter­est­ing thing here is that this works for any http sta­tus code. Just name the tem­plate after the http sta­tus code, and away you go.

Why might you want to do this? Well, the default Craft error han­dler will dis­play an error code and an error mes­sage. If the worst hap­pens, you might want to put a friend­lier face on it for your clients than just some­thing like:

Craft Cdbexception Error

Addi­tion­al­ly, some pen­e­tra­tion tests will flag this as a secu­ri­ty issue, because it’s giv­ing infor­ma­tion about the type of data­base used, the nature of the error, etc.

It’d be much bet­ter for every­one con­cerned if we just had a nice friend­ly error mes­sage, such as the one Twit­ter was famous for:

Twitter Fail Whale

We might not want to cre­ate a sep­a­rate tem­plate for every pos­si­ble http sta­tus code, though, so let’s dive a bit deep­er. This is exact­ly what Craft does when it looks for a tem­plate to ren­der 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 tem­plate that match­es the exact http sta­tus code. If that’s not found, then if it’s a 503 ser­vice not avail­able error, it looks for a tem­plate named offline (more on that lat­er), and final­ly it falls back on just look­ing for a tem­plate named error.

So per­fect! We can cre­ate error han­dling tem­plates for spe­cif­ic error codes that we want to han­dle in a spe­cial way, like 404 errors, and then we can have a gener­ic catch-all error.html or error.twig template!

Now we have an easy way to grace­ful­ly han­dle any errors that hap­pen in a user-friend­ly way, and we can con­trol what error infor­ma­tion is ever dis­played pub­licly. It’s not as cool as the Fail Whale, but it’s bet­ter than a scary CDbException error:

Nystudio107 Generic Error

N.B.: If you have dev­Mode on (which you should always have on when devel­op­ing in local dev), all Craft excep­tions will be dis­played using the Craft debug tem­plate. That means they will not be rout­ed to your cus­tom error pages. To test them, either turn devMode off, or just nav­i­gate direct­ly to the tem­plate in question.

Offline Stag­ing Sites

Ear­li­er we dis­cussed that a 503 ser­vice unavail­able error will result in Craft spe­cial-cas­ing for an offline.html or offline.twig template.

We can take advan­tage of this behav­ior to make sure our client stag­ing sites are not only not being index by Google­Bot and oth­er such crawlers, but also that only our client can see the web­site as we work on it.

In the Pre­vent­ing Google from Index­ing Stag­ing Sites arti­cle, we dis­cussed using robots.txt and a mul­ti-envi­ron­ment set­up to do this. Here’s anoth­er way to do it that you may find even more convenient.

Craft Cms Is System On

Craft has a con­fig set­ting called isSys­tem­Live (for­mer­ly isSystemOn) that we can set to false for stag­ing or oth­er envi­ron­ments that we don’t want any­one to access. This is a mul­ti-envi­ron­ment con­fig vari­able, that we might set some­thing 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 set­ting does is it caus­es Craft to return a 503 ser­vice unavail­able error code for any fron­tend request. This then caus­es Craft to throw an excep­tion, and dis­play the offline tem­plate. This is great, because we can con­trol what appears there.

So this stops Google­Bot, crawlers, and oth­er pry­ing eyes from see­ing the web­site as we work on it, but what about our clients?

Craft Offline Access Permissions

In the AdminCP, Set­tingsUsersUser Group lets you set access per­mis­sions for user groups. As long as your client has Access the site when the sys­tem is off per­mis­sion, they can log in to the AdminCP, and then access the site with aplomb.

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

Nystudio107 503 Error

You could even have your offline page be a fron­tend login form for your clients, to make it even eas­i­er for them.

In your code, you can even do con­di­tion­als based on isSystemLive too:


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

Wrap­ping Up

Since a theme of the arti­cle has been to use some well-known pearls of wis­dom, I leave you with one more:

The time you spend cod­ing defen­sive­ly and bul­let­proof­ing your web­sites will pay off with hap­py clients, and less frus­tra­tion for you as a developer.

Happy Baby Face

At the very least, future-you will be extreme­ly grate­ful that past-you did such a good job… because it’s almost inevitable that you’ll be the one work­ing 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

💖 💪 🙅 🚩
gaijinity
Andrew Welch

Posted on February 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related