Nested conditionals can make your code harder to read and change—especially if you nest them more than one level.
Below, I'll show an example of a function that returns a discount for the provided user.
- It should give a 20% discount for all regular users.
- But if the user has a premium membership, it will give a 40% discount.
- Gold users take the highest discount: 60%.
- If the user is banned, however, it should not give any discount.
Without guard clauses, I'll write this function like this:
function getDiscountForUser(user) {
let result
if (user.isBanned) {
result = 0
} else {
if (user.isPremium) {
result = 40
} else {
if (user.isGold) {
result = 60
} else {
result = 20
}
}
}
return result
}
I'll show you below how guard clauses can make it much simpler. But first, what are guard clauses?
What are Guard Clauses?
Guard clauses are the checks you put at the beginning of a function to check for the unusual conditions.
A usual (or called normal) condition is the condition that represents most cases for the needed behavior—in this case it's regular users getting a 20% discount. Anything other than that is called an unusual condition.
Guard clauses also return immediately if true—preventing other conditions from running.
Here's how the same function looks but with guard clauses:
function getDiscountForUser(user) {
if (user.isBanned) return 0
if (user.isPremium) return 40
if (user.isGold) return 60
return 20
}
With the beauty of guard clauses, the function became way much easier to read and change.
Top comments (9)
I completely agree about using guard statements, and how they can simplify things. In this particular example though, the code could be made more readable without introducing them.
By specifying the default state upfront, things can be simplified further:
Thanks for your comment. Actually, there's nothing wrong with your example, but the main idea of guard clauses is to communicate the intention of my code by showing what part is the normal flow. The normal flow is the what the majority of the cases would be, in this example it's the regular user account.
With guard clauses, I can show that I'm handling the special cases first before going to the main thing I'm working on. I think I could've made it more clear in this example by showing that the normal flow would usually contain more logic.
It's usually found that the unusual cases can return immediately with some value; that's why I put them at the beginning. And after that I can write all the needed computation code for the normal flow of the function.
So, when I look at this code, I say: "Ah! I see that this function has some special cases, and it's handling them by returning a value immediately. Ok, that means I should see the main code below these ifs.".
Another reason I prefer guard clauses is that it helps me reduce the number of mutable data. You can see that in my example, there's no longer a
result
variable in the function. Mutable data can usually cause bugs and confusion because you need to make sure that other parts of the code didn't modify it in an unexpected way. In this particular example, it's not a big deal since its scope is very small, and the code is very simple to reason about. But the more complex your code is, the harder it would be to reason about.Again, there's nothing wrong with your example, but I wanted to clarify why I chose guard clauses for this example and how they can help me view the intention of my code better. Thanks!
Thanks for your explanation. All good points that I agree with.
I regularly use guard statements, e.g. for checking user permissions on Web controller endpoints - if a user doesn't have permissions, return immediately. Another benefit is less indentation of code.
Agree with immutability too. I'll use
const
wherever I can, rather thanlet
orvar
. This becomes one of the main reasons that I use the ternary operator?
. However, if the logic is complex or results in nested ternary operators, I'll opt forlet
to make the overall flow more readable and debuggable.Polymorphism can remove conditionals, but you have to be careful of overusing it. If your conditionals are very simple (like the example in this article), then polymorphism can make them more complicated instead of simplifying them.
I'll show you below how polymorphism can remove if statements, but in case you're not familiar with it, check out this article I wrote: What is Polymorphism?.
There are two cases where replacing conditionals with polymorphism would improve your code:
It's not easy to explain the second case in a comment; it requires its own post. So, you can ignore the second case for now, and let me show you an example of the first one.
Polymorphism works by having different implementations for the same interface (I explained that in details in the post I shared above). So, if I were to take the example in this post, I need to create a base class for user and a subclass for each user type.
As I mentioned in the first point above, polymorphism is worth doing if you have repeated checks of the user type in many places of your code. But for simplicity, I'll only consider the function I'm doing the checks in:
getDiscountForUser
.To unify the interface of the user objects, then I need to add
getDiscountForUser
to the User class—but let's rename itgetDiscount
.Quick note: adding this method to the User class doesn't make sense because getting a discount is not the responsibility of the user model. But let's ignore that for demonstration purposes.
Finally, you can replace all the ifs in
getDiscountForUser
with justuser.getDiscount
method call.An important thing to note here (which you might be wondering about) is that there should be some factory function in your code that determines which user object to create. In the last code snippet above, I'm assuming that the
user
parameter is the correct user type. But that's determined in another place, usually a factory function, like this:I hope you found this answer helpful to you.
I agree that polymorphism should not be abused but the last factory method also uses a case statement violating open closed principle.
With the aid of polymorphism you can convert createUser() to an open and extensible solution without all those hardcoding factory classes.
The open-closed principle is a good one, but I wouldn't start applying all possible abstractions upfront. If you don't expect to have tens of subclasses that might grow in number in the future, a switch statement would be perfect.
After all, you should have tests for that part, and if you decided to store all the different subclasses in a map to remove the switch statement, then it should be easy to refactor to that.
Everyone has their own design style. For me I like YAGNI; so, I start with the simplest possible solution, and then improve the design incrementally as I see appropriate. By "the simplest possible solution" I mean the simplest abstractions that meet what I expect for a specific module to need in terms of architecture.
I think in this example, YAGNI and KISS will be the best solution.
Once you have good coverage and the business evolves you can do the refactor and remove the switch if necessary
A better alternative would probably be:
It also guide the developer to think in terms of small functions instead of large ones.
you can improve this solution by removing all ifs with polymorphism