If-statements design: guard clauses may be all you need

Serż Kwiatkowski
5 min readJan 29, 2018

--

There is a lot of debate on how the if-statements should be used for better code clarity and readability. Most of them boil down to an opinion that its completely subjective and aesthetic. And usually that’s true, but sometimes we still can find a common ground. This post aims to derive a small set of advices for a few such cases. Hopefully this will reduce the amount of thinking and debating when writing something as simple as an if-statement.

Guard Clauses

Probably the most documented case is the guard clause, also known as assert or precondition. The idea is that when you have something to assert in the beginning of a method — do this using a fast return.

public void run( Timer timer ) {
if( !timer.isEnabled() )
return;

if( !timer.valid() )
throw new InvalidTimerException();
timer.run();
}

Despite looking obvious, it is often left behind due to the fact that our brains function differently: we tend to think “run the timer if it’s enabled”, not “run the timer, but if it’s not enabled do nothing”. However, in the code, the latter leads to a better separation of corner cases from the main method logic, if we strictly follow that only something asserted in the beginning can be considered a guard clause.

As appears, the guard clause is not nearly a new idea. Yet I see it as one of the most straight-forward and simple ones, which can be accepted without a lot of debate. But the biggest revelation for me was that most of the cases when if-statement is used can be transformed to use a guard clause instead, and this is what I’m going to elaborate on.

Large if-blocks

It is pretty common to find something like the following in a codebase:

if( something.isEnabled() ) {
// pretty
// long
// logic
// of
// running
// something
}

Usually it isn’t designed this way from the beginning. Instead, such blocks appear when a new requirement appears which says “we should run that something only in case of X”. The most straight-forward solution, of course, would be to wrap the “run that something” part into an if-statement. Easy.

The problem is that actually it’s only easy to write, but not to read. See, to understand the code one needs to derive the whole picture from it, a concept to further work with — because humans are much better in operating on concepts, not on algorithms. Introducing this kind of condition in the beginning creates an additional context, which has to be kept in mind when working with the main method logic. Obviously enough, we should strive to reduce the amount of mental context required at any given moment.

Another big problem arises when the logic of such code becomes long enough to not fit the screen vertically. At some point you may even forget that it’s there, which would completely break your picture.

To avoid all of this, you just need to invert the if-statement so that the condition would become a guard clause:

if( !something.isEnabled() )
return;
// same
// long
// logic
// of
// running
// something

Again, there’s a separation of concerns: first you get rid of the preconditions at the beginning (throwing them away from your mind, too — which is important to better focus on the main logic), and then just do what the method is required to do.

Returning in the middle

Multiple returns are considered a bad idea for reasons, and this is probably one of the biggest. Contrary to a return in a guard clause, a return or throw in the middle of the method is not so easily detected at the first sight. Like any other conditional logic, it complicates the process of building a mental picture of the method. Moreover, it requires you to consider that under some circumstances, the main logic of this method will not be executed completely, and every time you’ll have to decide: should you put the new code before or after this return?

Here’s an example of some real code:

public void executeTimer( String timerId ) {
logger.debug( "Executing timer with ID {}", timerId );
TimerEntity timerEntity = timerRepository.find( timerId ); logger.debug( "Found TimerEntity {} for timer ID {}", timerEntity, timerId ); if( timerEntity == null )
return;
Timer timer = Timer.fromEntity( timerEntity );
timersInvoker.execute( timer );
}

Looks like a precondition, doesn’t it? Let’s put it to where a precondition has to be:

public void executeTimer( String timerId ) {
logger.debug( "Executing timer with ID {}", timerId );
TimerEntity timerEntity = timerRepository.find( timerId ); logger.debug( "Found TimerEntity {} for timer ID {}", timerEntity, timerId ); executeTimer( timerEntity );
}
private void executeTimer( TimerEntity timerEntity ) {
if( timerEntity == null )
return;
Timer timer = Timer.fromEntity( timerEntity );
timersInvoker.execute( timer );
}

This refactored code is much easier to read, since there are no conditions at all in the main logic, there’s just a mere sequence of simple actions. What I wanted to show is that it’s almost always possible to split a method which has a return in the middle so that the preconditions are nicely separated from the main logic.

Small conditional actions

It is also a common practice to have lots of small conditional statements like this:

if( timer.getMode() != TimerMode.DRAFT )
timer.validate();

This is totally legit in general case, but often is just a hidden precondition which should better be placed in a method itself. You’ll probably notice this further, when each time you invoke the method you need to add this if-statement because the business logic says so. Considering that, the proper solution would be:

public void validate() {
if( mode == TimerMode.DRAFT )
return;

// validation logic
}

And the usage is now as simple as:

timer.validate();

Conclusion

The use of guard clauses is a good practice to avoid unnecessary branching, and thus make your code more lean and readable. I believe if these small advices are considered systematically, it will greatly decrease the maintenance cost of your piece of software.

And at last, generally, there’s nothing bad in having a conditional statement here or there in your code. Though my experience shows that if you never care about trying to avoid that, then at some point you’ll end up wasting hours building a mental picture of a complex highly branched piece of code while trying to put that next thing a business wants into it.

--

--