…most of the time. Let me explain.
If you studied computer science, you're well aware of the if/else control flow. It was drilled so far into my head for all four years of university it feels like second nature. But, after working as a software engineer, I realized that using else is often a sign of poorly-written code.
Consider this simple function that creates a user record in a database:
func (db *DB) createUser(user *User) error {
if db.IsConnected() {
// create the user
return nil
} else {
return fmt.Errorf("database not connected")
}
}
If you're not familiar with Go, all this function is doing is creating the user if the database is connected, and raising an error if not. You can ignore the syntactic sugar of receiver functions, pointers, and the error return type as these are specific to Go. The ideas are the same as your preferred language of choice.
Have you noticed the issue with coding like this?
It's not obvious in this example, but the problem arises if there are more conditionals, e.g. if you need to check that the user's email is unique. With each new condition, you add another layer of indentation to the core logic. Eventually, it becomes hard to figure out what condition is attached to what error or return, and how the code actually flows.
This is where guard clauses save the day.
What is a guard clause?
A guard clause is made up of two parts, the guard condition and the guard handler. The guard condition is tested when the function runs. If it evaluates to true, then the execution moves into the guard handler. Otherwise, the execution jumps over the handler and moves on to the rest of the function.
The guard handler does two tasks: documenting what went wrong and stoping the execution of the function. In most languages, particularly ones with exceptions, these can be done in the same step. Go handles it similarly, by returning an error that includes the context of what went wrong.
After the guard clause you know the condition must have been true, because otherwise the function execution would've been stopped. This frees you up to perform whatever logic is required in the top-level of the function, instead of within the body of an if statement.
Consider the function above. How would you refactor it to use a guard clause?
Here's how I did it:
func (db *DB) createUser(user *User) error {
if !db.IsConnected() {
return fmt.Errorf("database not connected")
}
// create the user
return nil
}
The first if statement is the guard clause, it'll always return an error when the database isn't connected. After the execution continues past the clause, we know that db.IsConnected()
must be true. Therefore, can implement our core logic without having to check the connectivity status every time we use the database.
Some other added benefits of using a guard clause are:
- The core logic is on the initial indentation level.
- It establishes a contract of properties that must be met before the core logic is executed.
- Adding new conditions is easier and cleaner in git diffs, leading to code that is easier to read and review.
We've already looked at the first point, so let's take a deeper look at the last two points.
Establishes a Contract
When you want to use a function, how do you know what conditions must be met before it'll work? Simple answer, you don't. The longer answer is the code author should try to help you out. For example, they may document the requirements in a comment above the function signature. But if you've ever worked on a large project, you know that documentation has a tendency to get out of sync with the implementation.
Guard clauses help with this by including the requirements within the code itself, which means the they will never get out of sync. It allows future developers to understand what a function needs to work at a glance, without the tedious back and forth of referencing documentation and making sure the documentation has been updated in sync with the code.
If you've ever heard the term Design by Contract, this is very similar to that idea. Given a specific set of inputs, that are checked by the guard clauses when the function runs, and assuming there are no side effects, you ensure a valid and accurate output. Working this way allows future developers to know how they need to implement their features to be in compliance with the functions they want to use.
Adding New Conditions
Imagine you're adding another condition to the function. How would you do it in the original function (without the guard clause) vs. in the guarded function?
In the unguarded function, you'd have to add another, nested, if/else statement within the original conditional, which adds another level of indentation and obfuscates the intent of the function. Take a look at the git diff below:
func (db *DB) createUser(user *User) error {
if db.IsConnected() {
- // create the user
- return nil
+ if !db.UserExists(user) {
+ // create the user
+ return nil
+ } else {
+ return fmt.Errorf("user already exists")
+ }
} else {
return fmt.Errorf("database not connected")
}
}
Compare this to the when we use guard clauses:
func (db *DB) createUser(user *User) error {
if !db.IsConnected() {
return fmt.Errorf("database not connected")
}
+
+ if db.UserExists(user) {
+ return fmt.Errorf("user already exists")
+ }
// create the user
return nil
}
The diff for the guarded function is much easier to read. If you've ever done a code review, you can see how much easier the latter function is to review, too. Being able to understand what a function does at a glance is essential to the maintainability, and that's exactly what guard functions do. If you want a maintainable system, use guard clauses.
When can you use else?
Just like every other rule in software development, there's times when you should use else
instead of a guard clause. My rule of thumb for when to use a guard clause vs. using an else statement is: if it alters the flow of a program, use a guard clause. So if you need to log an error, or call a setup function, etc., use a guard clause. If you just need to log that a non-essential component of the application failed, for example, use an else
.
Below is an example of logging a failure.
func StartApplication() {
analyticsConnected := analytics.Connect()
if analyticsConnected {
log.Println("analytics connected")
} else {
log.Println("failed to connect to analytics service")
}
// continue with starting the application
}
While some marketing people may argue that collecting analytics is the most important part of an application, us developers know that the application will work just fine without them. So when we start an application, if our analytics fail, we can continue with startup and retry starting the analytics later.
Conclusion
Despite having if/else drilled into my brain during university, in my career I have found that I rarely use the if/else flow. Instead, I opt for guard clauses, and I hope you have been convinced to use them, too. Personally, I consider using else
a sign of bad code, but obviously it is situation dependent. Whatever your coding style, I encourage you to give guard clauses a try and see how they simplify your code.
If you have any questions or comments, I'd love to hear them! Feel free to leave a comment below or shoot me a message on Twitter.
Cover photo by Markus Spiske on Unsplash
Top comments (6)
Luckily I was exposed to the if-guards in a summer intern job before ending my studies (the engineer who taught me it called it "paranoia checks"). Indeed the code gets a lot cleaner and more straightforward to read and comprehend.
Another non-neglible benefit of this, is that the rest of the code (the one who would end up in an else clause) doesn't need to be indented. Giving more horisontal space for the code.
I won't say
else
is bad code. Sometime you have to use it. But for most of the time I prefer early escape for all.else
is the trump card I only use when my others is all gone :))If I really have to use 'else' and if the logic fits under my 120 character margin as a nifty oneliner, I would prefer to use ternary operator
You over simplified the issue IMO.
I can do stuff without
if
at all:There are times where
if
is needed:But There are times when
if
andelse
must be used:How would you do it without
else
?Those are all good points and as I mentioned in the post there are exceptions to the rule, where you should use
else
. Your final snippet, for example, is an excellent time to use else instead of a guard clause.As I pointed out, avoiding
else
leads to code that is easier to read and maintain and I don't see a downside of that. Dogmatic application of any rule is a fallacy of any software engineer. If we were to avoid a rule just because there are exceptions to it, software engineering would be lawless 😄Thanks for the "cleaner Git diff" point. Somehow I am strugling to adopt guard clauses over nested if-else, but this is by far the most valid and important advantage I have read.