My name is Jean-Michel and I am a committed engineer at Per Angusta. I have tried to summarise my thoughts and experience about good and bad practices of the Guard Clause pattern, starting by common assumptions to applicable examples with code samples.
Basically, a common mistake with Guard Clauses is to think that they are simple substitute for “if…else” statements. Of course they consist in “if…else” statements, but it’s not a simple musical game of chairs and it actually involves a design pattern with special meanings and consequences on code quality.
What is a Guard Clause?
A Guard Clause, also known as Early Return or Bouncer Pattern, is a common practice in programming, consisting in an early exit of a function based on preconditions check.
It is nothing other than an “if…else” statement, but it provides with two benefits: It makes the code flatter by reducing number of indentation levels, and it allows exceptions handling to be placed at the beginning, making subroutines clearer to read.
It is not limited to a specific programming language, however practices are more or less common across communities. For instance, in C programming, their usage is very common due to the basic need to handle null-pointers, but also due to strong Unix conventions that involves restriction of functions’ lengths in width and height (to facilitate edition within a terminal) and in order to optimise compilation time.
When to use a Guard Clause?
I would summarise the answer into 3 fundamental assumptions:
- It helps with readability
- It has a strong and obvious link with the scope of the method
- It solves a significant problem (performance, stability, business issue)
If a guard clause does not verify at least 2 of those 3 assumptions, I would consider there probably is a bad design somewhere, and you should consider to split your algorithm into multiple subroutines (or simply not to use Guard Clause).
I would add a tip: If a guard clause is not present at the beginning of a method, there are serious chances that you have a bad design somewhere. But, be aware it’s not systematic.
1. It helps with readability
Implementing Guard Clauses while losing readability is almost a non-sense. Their existence is an answer to the lack of readability of nested “if…else” statements and high levels of indentation.
I would say a Guard Clause that reduces the readability must have strong other reasons, such as stability or performance benefit. And if you feel not comfortable with a convention that forces you to use a Guard Clause rather than something more readable, ask your team if it would better be to adjust the convention instead of the code.
(Note that in this article, I chose the Ruby language to give examples, because it is my natural programming language. But each example applies to all languages)
❌ The following example shows a method that includes two conditional actions, but the Guard Clause in there results in different indentations for both conditions’ and actions’ contents, reading is made difficult:
def do_multiple_things
if valid_context_for_first_action?
# action 1
end
return if !valid_context_for_second_action?
# action 2
end
2. It has a strong and obvious link with the scope of the method
The ideal use case of a Guard Clause is when it highlights a reason for not having called the given context at all. I mean, when a given context as a whole does not even make sense with current subject.
This point is particularly involved by inheritance and shared logic. For instance, a set of shared validations can require exceptions with certain models. Or a class inheriting of parent’s methods can require additional conditions of applicability.
In other words: A guard clause should allow a developer to quickly understand that even the method’s name is a non-sense for the current subject.
✅ The following example shows a method that early exits due to a missing piece of critical information for the purpose:
def send_welcome_email
return if @user.email.blank
UserMailer.with(user: @user).welcome_email
end
3. It solves a significant problem
Third point is about pragmatism and efficiency. In certain circumstances, you would think about implementing a Guard Clause to solve an issue in relationship with the global context, whose scope is larger than the local context.
Exiting a method, or skipping an element in a loop, due to external factors or attempted limits, is a good use case for Guard Clauses because they actually highlight a major concern of applicability.
✅ The following example shows a method that exits from a nested level due to a major limit:
def notify_many_third_parties_with_many_records
@notifiable_records.each do |record|
return if API.daily_limit_reached?
API.notify_third_parties_with_record
end
end
Don’t be dogmatic, don’t forget to think
Every language has its own set of coding style and conventions. Your team may also have its own: max number of lines per file, naming conventions, order of inclusion, etc.
Static code analysers such as Rubocop, for Ruby programming, or norminette for 42 School’s exercices, provide packaged rules to help developers to follow conventions by highlighting where in the code base some are not respected. Rules are usually configurable to comply with the needs, and there are often default settings issued by the community.
As a result, a community with such popular tools like Ruby’s can see a lot of projects and libraries following similar code styles, whoever has coded them, whatever their level of coding skill is, whatever the maturity of the project is. But not to follow a convention can make sense in many cases, such as:
- This method is too largely inspired by an open source algorithm: prefer to keep the spirit of its origin so that it is easier to compare when updates or bugs occur.
- This file is legacy, well tested, and not supposed to change so many times: no need to invest time in rewriting it.
- This project is small and not intended to grow: keep code readable as it deserves and that’s fine!
Therefore, there is a lot of situations where developers should better beside a static convention. Especially when pros and cons are not mastered, when intentions behind rules are not explained and cristal clear. As an example of that, the following static rule often brings me some headaches in code reviews:
Style/GuardClause: Use a guard clause instead of wrapping the code inside a conditional expression.
Our static code analyser does obviously not trigger this warning on every «if» statement of the application, but does warn all «if» statements placed at the end of methods when they have no «else» statement. In most of the cases, fixing that warning by writing a Guard Clause is counter-productive because of a root cause largely unrelated with the concern of the pattern: a lack of separation of concerns. Often, the solution I request is to disable warning, not to rearrange the code. Below examples may help you understand which solutions to choose.
Examples with code samples
Methods with single concern
✅ The following example looks fine because it is clearly designed: One method with single role:
def validate_dates_consistency
return if start_on.blank? || end_on.blank?
return if start_on <= end_on
errors.add :end_on, 'must be greater than Start Date'
end
Methods with multiple concerns
❌ The following example is ambiguous, it is a mix of conditional statement and Guard Clause, without any hierarchy or special meaning:
def validate_complex_situation
if something_wrong?
errors.add :base, 'You must correct something'
end
return if !something_else_wrong?
errors.add :base, 'You must correct something else'
end
✅ Think readability and solve it by using either simple “if” statements…
def validate_complex_situation
if something_wrong?
errors.add :base, 'You must correct something'
end
if something_else_wrong?
errors.add :base, 'You must correct something else'
end
end
…or split into multiple components:
def validate_complex_situation
handle_errors_about_something
handle_errors_about_something_else
end
def handle_errors_about_something
return if everything_valid?
errors.add :base, 'You must correct something'
end
def handle_errors_about_something_else
return if everything_else_valid?
errors.add :base, 'You must correct something else'
end
Loops with multiple transformations
✅ The following example involves a sequential algorithm, where the role of guard clauses is directly linked to the context of the method (a sequential list of similar actions to be performed on each record), and readability is also present:
tasks.each do |task|
task.mark_as_read!
next if task.completed?
task.mark_as_pending!
next if task.almost_completed?
task.mark_as_quickwin!
end
❌ The following example falls into a previous one, where a loop has multiple roles and the guard clause’s role is to handle a specific case. It is hard to read, it has a distant link with the context and it does not solve any problem:
very_important_tasks = []
tasks.each do |task|
task.mark_as_read!
if !task.completed?
task.mark_as_pending!
end
if task.owner != current_user
task.notify_owner_of_read_event_by_current_user
end
next if !task.involves_administrator?
very_important_tasks << task
end
Significant problem solved
✅ The following example involves an expensive call to a huge service called “SyncTaskWithThirdParties”, and a performance profiling of the application has highlighted that it was the root cause of significant useless memory allocations with particular conditions. Readability is ensured through comments and test suite, whereas the link between the role of the method and the content of the guard clause can be hard to understand:
def sync_with_third_parties
# avoid an expensive call to the sync service (15% of use cases)
# please refer to the documented specs in task_spec.rb
return if start_on.blank? && owner.is_a?(AdminUser)
::Services::SyncTaskWithThirdParties.new(self).call
end
✅ The following example involves a global context of maintenance window, which is making the role of the method not efficient or even not applicable, so that exiting the method is made necessary and understandable:
def invalid_cache!
# we have some issues with cache during maintenance windows
# we prefer to save performance of display with obsolete data
return if CacheProvider.maintenance_window?
update_columns(cached_at: Time.zone.now)
end
Inherited methods
✅ The following example involves an overridden method with a subroutine that does not make sense in a subclass’ context and should be skipped:
def clean_parameters
# skip whole process with action not requiring parameters at all
return if action_name == 'toggle_visibility'
super
end
❌ The following example is ambiguous and malformed because the Guard Clause does not comply with any rule: The method “clean_parameters” is still applicable, it does still make sense with children class’ subject. We only want to handle a specific case, which is not a significant problem. Plus, we lost ease of maintainability:
def clean_parameters
super
return if !@params.key?(:send_notification)
@send_notification = @params.delete(:send_notification) == 'true'
end
✅ As a solution, forget Guard Clause pattern. Keep the ability to implement additional logic after the conditional statement…
def clean_parameters
super
if @params.key?(:send_notification)
@send_notification = @params.delete(:send_notification) == 'true'
end
# additional logic here
end
…or extract the specific case into a separate context, where a Guard Clause actually makes sense. Plus, additional logic can later be easily appended:
def clean_parameters
super
clean_parameter_send_notification
# additional logic here
end
def clean_parameter_send_notification
return if !@params.key?(:send_notification)
@send_notification = @params.delete(:send_notification) == 'true'
end
Conclusion
Guard Clause is a design pattern, with meanings and consequences on a code base for the long-term. It is not a code style rule as you would follow with indentation (“2 spaces” or “1 tab”, applicable everywhere, mechanically).
Guard Clauses are related to code structuring and separation of concerns. Be pragmatic with their usage!
Interested in joining an enthusiastic engineering team? We are recruiting talented people! Learn more about the team and our open positions here
Top comments (0)