Why you should always use guard statements
23 Apr 2024 (procedural-programming, code-style)
The real world is messy, and real world software has to deal with that messiness. Handling possible erroneous states can result in complex and unwieldy conditional logic. This is especially true when using languages with nulls and untyped languages, like JavaScript. You find yourself frequently adding if-statements to introspect types and check for nulls. Often in order for a function to run successfully, its arguments need to meet some specific conditions, and you need to check that these are met otherwise you might cause an error at runtime.
Further, for each check that might fail, you also want to log or throw an error to provide information about what went wrong. For this reason, the conditions can’t be composed together as a boolean expression, they need to be performed separately so that the error that gets thrown is specific to the check that failed. Performing these checks one after another quickly leads to deeply nested if-statements that make code hard to read. However, guard statements are a solution that is easy to implement and makes for code that is far easier to read and maintain.
I’ll give an example of the situation I’ve described. Let’s say we’re
building a piece of software in which we hold details about people, and
we have a function called setJob
which, as you might have
guessed, takes person
and job
as arguments and
sets the person’s job.
function setJob(person, job) {
.job = job;
person }
We’re using JavaScript, so although person
should be an
object representing a person and job
should be a string,
our only way to check this is to do so manually in if-statements.
function setJob(person, job) {
if (typeof person === 'object') {
if (typeof job === 'string') {
.job = job;
personelse {
} throw new Error(`${job} is not a string`);
}else {
} throw new Error(`${person} is not a Person object`);
} }
There are also certain states the person
object might be
in which should prevent us from being able to set their job. For
example, if the person is retired, or if they are not an adult.
function setJob(person, job) {
if (typeof person === 'object') {
if (typeof job === 'string') {
if (!person.retired) {
if (person.age >= 18) {
.job = job;
personelse {
} throw new Error('Cannot set the job of someone under 18.');
}else {
} throw new Error('Cannot set the job of a retired person.');
}else {
} throw new Error(`${job} is not a string`);
}else {
} throw new Error(`${person} is not a Person object`);
} }
This has become a very unwieldy piece of code. There are 3 major problems with this style, that I can see:
- The function’s happy path is the expression
person.job = job
, but because this is buried in deeply nested if-statements, it isn’t easy to identify it. - Because of the nested structure, each if-statement’s
else
block is separated from its condition. The more nested ifs we add, the harder it is to match up a condition with itselse
block. - Just like when reading a text with nested sections, there is mental overhead involved in holding the context of each layer of nesting in your head, which could be reduced by using a flatter structure.
Let’s refactor the code to use guard statements and compare. To
switch to the guard statement style, we have to invert all of the
conditions; we’ll now be performing the opposites of our previous
checks. Then, because throw
stops the execution of the
function, none of the checks need an else
block, as any
code that comes after the throw
will necessarily not run.
This works when failed checks result in a return
statement
too, as returning early also halts the function’s execution.
function setJob(person, job) {
if (typeof person !== 'object') {
throw new Error(`${person} is not a Person object.`);
}
if (typeof job !== 'string') {
throw new Error(`${job} is not a string.`);
}
if (person.retired) {
throw new Error('Cannot set the job of a retired person.');
}
if (person.age < 18) {
throw new Error('Cannot set the job of someone under 18.');
}
.job = job;
person }
As a result, we’ve removed the nested structure and switched to a
flat list of conditions which is much easier to navigate. If we need to
add more conditions, the function gets longer, not deeper—like adding
more items to a list, not embedding more sections into an existing
section. And unlike in the previous version, the result of a failed
check (the throw
) sits right next to the condition; we can
more easily see what happens if a check fails. Furthermore, the happy
path sits at the end of the function, which makes sense semantically as
it sits after all of the checks it depends on. This will always be the
case if guard statements are used consistently—the structure of a
function is always “perform checks” THEN “execute happy path”.
This style of code is sometimes referred to as “never-nesting”, which is a slight misnomer, as we don’t remove all the nesting from our code when we follow this pattern. The point, I think, is to acknowledge that when you add a nested block, you’re creating a nested context for a block of code to exist in. That context, and all of the higher contexts the block is within, have to be kept in mind when navigating the code. Using a flat, non-nested structure allows the code to read like a simple checklist, rather than like a complex legal document with sections within sections. Your function lists some criteria that have to be met, then it does some work. For that reason, if we can reasonably remove a layer of nesting, it is good to do it.
I have seen certain cases where using a guard statement would lead to less expressive code than nesting if-statements, but this is not the norm. I think it is better to treat guard statements as the default and only introduce nesting where there is a clear reason to do so.