When (not) to use guard
December 14, 2015
Hey! This is an old post. It reflects my views and the state of the world in 2015, but is likely to be outdated. You’ll find me
on Twitter, building
Nozbe, and making
engineering blueprints.
guard
is one of my favorite features of Swift 2. It’s one of those subtle syntactical constructs we could totally do without. And yet having it is such a delightful win. It makes our methods cleaner and easier to read, it helps express the “early exit” intention, and adds a little extra safety.
However, it’s important to learn and understand how to use guard
properly. It has its place, but it’s not meant to replace if..else
and if let
in all cases. No matter how great guard
is, it can easily be misapplied and forced into places where other constructs do a better job.
Here are some basic guidelines for when to guard
, and when not to:
Do: verify entry conditions
This is perhaps the simplest and most common use case for guard
. You have a function that does a specific job, but only when one or a number of basic conditions are met at the beginning.
For example:
This has two advantages:
First of all, by putting the check at the top of the function, instead of wrapping the whole method in an if
, we’re conveying that the condition isn’t part of the fundamental job the function does. Instead, it’s just a basic entry condition.
Second of all, using guard
suggests to the reader, and ensures at compile time, that if the condition is false, the method will return. The compiler check, albeit subtle, helps with long-term maintainability of the code — if someone accidentally removes the early exit from the else
clause, the resulting bug will be caught immediately.
Do: early exits on a long success path
Use case: a function with a relatively long “happy path”, but with one or more checks that will be met in the best case scenario, but should just return or throw an error if false.
Do: unwrap optionals (flatten if let..else
pyramids)
Similar scenario: you need an entry condition, or checks scattered between code on a long happy path. But instead of boolean conditions, you want to verify that some optionals have a value and unwrap them:
Pro-Tip: there are better ways of dealing with JSON in Swift.
Unwrapping optionals is where guard
really shines. Unlike if let
, which unwraps the value for use in between if
’s curly braces, guard
adds unwrapped values to the outer scope — so you can just use them after guard
, not inside.
The reason why this is great is because if you have more than one instance of optional unwrapping, you can avoid the pyramid of doom (multiple levels of nested if let
s).
In non-trivial cases, it’s easier for our brains to follow a flat path with a few early exits rather than analyze nested branches.
Do: return and throw
As a general rule, an early exit means one of three things:
Execution was interrupted
When your method returns no value, merely executes a command, but it cannot be completed.
Example: a function that updates WatchKit application context, but the iOS app is not connected to any Apple Watch.
Do: just return
The result of a computation is an empty value
When your method returns something, possibly by transforming the input parameter, and the transformation can’t be carried out completely.
Example: a function that returns an array of objects deserialized from a cache, but there’s no cache saved on disk.
Do:
return nil
return []
, return ""
— empty values of standard library containers
return Account.guestAccount()
— custom objects representing a default/empty state
The computation failed with an error
When your method can fail in more than one non-trivial way and you want to communicate to the caller the reason for failure.
Example: a method that reads a file from disk, or a method that takes a network response and parses it.
Do:
throw FileError.NotFound
return Result.Failure(.NotFound)
— if you’re into using typed Result values
onFailure(.NotFound); return
— for asynchronous calls
return Promise(error: FileError.NotFound)
— asynchronous calls using Promises
Do: log, crash, and assert
Logging
Sometimes it makes sense to log a diagnostic message to the console before returning early, at least during development. This helps you track failure states, even if your code handles them correctly. However, it’s rarely appropriate to have more code in guard
’s else
block than that.
Fatal conditions
If a failure to fulfill a condition is a serious programming error, it might make sense to purposefully crash your program. If your app will crash either way, or will end up in an illegal state, it’s usually better to do it yourself. This way, you can reliably exit in a known location and display a reason for the crash.
Usually, the way to do it is using precondition
:
However, if the condition involves unwrapping optionals, not just a simple boolean expression, use guard
:
Assertions
Sometimes, a condition is always expected to be fulfilled, but a failure to do so isn’t a serious programming error. In that case, consider using assertionFailure
like so:
This way, you’ll crash and easily catch a bug during development and internal testing, but in production, the app won’t crash (even if it will be buggy).
Again, if the condition is purely boolean, assert(condition)
will usually do the job.
Don’t: use guard for trivial if..else
expressions
When you have a trivial method that only contains a single, simple if..else
expression, don’t use guard
.
For such simple cases, it’s much easier to understand and conceptualize a two-branch if..else
expression rather than the flattened version — despite otherwise being a good candidate for a guard
.
Pro-Tip: Make sure you understand optional chaining, Optional.map
, and Optional.flatMap
— you can often avoid having an explicit if let
altogether with those tools.
Don’t: use guard
as a “reverse if”
Some languages, such as Ruby, have an unless
statement, which is essentially a “reverse if
” — the code inside the statement is executed when passed condition evaluates to false
.
Swift’s guard
, despite a few similarities, is not the same thing. guard
is not a general-purpose branching mechanism. It’s specifically meant to be used for early exits when expected conditions fail.
Even in situations where you can bend guard
to act as a reverse if
, don’t. Just use if..else
or consider splitting your code into multiple functions.
Don’t: put complex code in else
clause
This is a corollary of the previous rule:
guard
’s else
clause shouldn’t be more than a simple early exit. It’s OK to make diagnostic logs, but rarely anything else. It’s also fine to clean up any unfinished work or opened resources, although most of the time you should use defer
for that.
But essentially, if you do any real work in the else
block, aside from what’s needed to just get out of the function, you’re misusing guard
.
Rule of thumb: guard
’s else
clause should almost never have more than two or three lines of code.
Published December 14, 2015.
Send feedback.