The Guard Clause

None shall pass.

Nov 1, 2020

Palace Guards
Photo by Lloyd Blunk on Unsplash

The World Is Weird

In the real world, our software will inevitably encounter scenarios and data states that are unexpected or unsupportive to our applications. Things that are expected to exist do not, values that are expected to be valid are invalid, or there may be odd but expected edge cases that must be handled.

As good software engineers we anticipate these things, and we write our software to handle these scenarios to avoid preventable runtime errors or otherwise bad user experiences.

The Happy Path

In software development we often refer to the 'happy path' as the best case pathway through a system. The term is used at a macro level – for example when referring to users of a website performing some action, and also at a micro level – for example when referring to the flow of logic through an individual function within the application code.

The inverse of the happy path is appropriately known as the 'unhappy path', or more often the pluralized 'unhappy paths', as there is usually more than one non best case pathway. Also note that these other paths may not always be considered to be bad or failures, just sub-optimal as compared to the happy path.

Often when we build our software, all of the additional handling that may be required in anticipation of failure or edge cases can cause our happy path to become difficult to read, difficult to understand, and difficult to reason about. This can make it easier for bugs to manifest, and harder to maintain or extend upon our code.

The Family Path

To illustrate this issue, let's imagine a function – getFirstBornChildSentence – which takes as input information about a parent and their children, and returns a sentence of the form: "[parent name]'s first born child is [child name]".

For this example, the failure or edge cases we need to consider are:
  • The parent object is null – we should throw an error.
  • The children array is null – we should throw an error.
  • The children array is empty – we should return a sentence like "[parent name] has no children."
  • The parent object has no name – we should return a sentence like "The first born child is [child name]."
  • The first born child object has no name – we should return a sentence like "[parent name]'s first born child is [child age] years old."
  • One or more child objects have no age – we should throw an error.

We will also need to handle combinations of the above cases as necessary – for example, if the children array is empty and the parent object also has no name, then we should return the sentence "There are no children".

Here's what an initial implementation might look like:

All of our failure and edge cases are handled, and the function works as expected. Unfortunately, it's not what I would call easy to read...

A couple of key issues with this implementation are:
  • The reader's eyes must constantly dart between the if and else blocks, which is not only a strain but also makes it easier to lose one's place in the code.
  • It may not be clear what the happy path is here – that is to say, it might not be clear what this function will return in the ideal, best case scenario. With this implementation style the way to identify this is to look for the innermost conditional block. In this case if (parent.name && firstBornChild.name) { ... } is the innermost conditional, so the value returned within this block is the ideal return value – the final statement on the happy path. This is not straightforward to reason about though, and the reader is unlikely to determine this easily (if at all).

The Guard Clause

While we cannot remove our conditional checks – after all, we need these to handle the failure and edge cases – we can simplify our code significantly by replacing the nesting of our conditional checks with the 'guard clause' pattern.

A guard clause is a conditional check which must evaluate to false in order for the logical flow to continue onward. If the check evaluates to true, then any code after the guard clause will never be reached. I.e. Within the guard clause block, the function must return (or throw).

Here's a revised implementation of our function using guard clauses:

Using guard clauses, we've removed all of our nested conditionals – well, almost all of them. In this case it is useful to maintain some nested conditionals to group secondary edge cases within a certain primary edge case, to reduce noise and make the primary edge cases more obvious. Also note that when converting to guard clauses, all of our conditionals have necessarily been negated.

With our new implementation, we've improved upon both key issues mentioned previously:
  • The reader no longer needs to jump up and down between if and else blocks – the reading flows naturally from top to bottom.
  • The happy path is much easier to reason about. The ideal return value – the final statement on the happy path – is simply the last statement in the function!

Aside from the improvements to readability and understandability, using guard clauses also makes implementation much simpler. You can effectively implement your code from the bottom up, starting with the happy path, and then adding guard clauses wherever necessary.

Conclusion

With the guard clause pattern we can simplify complex, conditional-heavy code, making it easier to read and understand. This ultimately leads to a lower likelihood of introducing bugs, and improved maintainability and extendability.

En Garde! 🤺

Thanks for reading!

Read more posts like this

Built with Sapper. Styled with Tailwind. Powered by Vercel.