DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

1

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

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

Image of Datadog

Master Mobile Monitoring for iOS Apps

Monitor your app’s health with real-time insights into crash-free rates, start times, and more. Optimize performance and prevent user churn by addressing critical issues like app hangs, and ANRs. Learn how to keep your iOS app running smoothly across all devices by downloading this eBook.

Get The eBook

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay