The Guard Clause
None shall pass.
Nov 1, 2020
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]".
parent
object is null
– we should throw an error. children
array is null
– we should throw an error. children
array is empty – we should return a sentence like "[parent name] has no children." parent
object has no name
– we should return a sentence like "The first born child is [child name]." name
– we should return a sentence like "[parent name]'s first born child is [child age] years old." 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...
if
and else
blocks, which is not only a strain but also makes it easier to lose one's place in the code. 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.
if
and else
blocks – the reading flows naturally from top to bottom.
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