DEV Community

Cover image for JS Refactoring Combo: Replace Nested If-Else with Guards
Lars Grammel for P42

Posted on • Edited on • Originally published at p42.ai

JS Refactoring Combo: Replace Nested If-Else with Guards

Nested if-else statements can make it unnecessarily complicated to reason about the different executions paths and results of a function. A loss of productivity and the introduction of bugs due to misunderstandings can be the outcome.

The refactoring "Replace Nested Conditional with Guard Clauses" from Martin Fowler's book "Refactoring - Improving the Design of Existing Code (2nd Edition)" can help in those situations.

A guard clause checks for a condition and returns from the function if the condition is true, potentially doing some computation and returning a result. It makes it easier to reason about the function by ending one execution path early.

Here is an example function (from Replace Nested Conditional with Guard Clauses, Martin Fowler) before refactoring:

function getPayAmount() {
  let result;
  if (isDead)
    result = deadAmount();
  else {
    if (isSeparated)
      result = separatedAmount();
    else {
      if (isRetired)
        result = retiredAmount();
      else
        result = normalPayAmount();
    }
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

In this example, three nested if-else statements check different conditions (e.g., isSeparated) and update a variable result. The variable is returned at the end of the function.

The goal is to refactor the function into the following version:

function getPayAmount() {
  if (isDead) return deadAmount();
  if (isSeparated) return separatedAmount();
  if (isRetired) return retiredAmount();
  return normalPayAmount();
}
Enter fullscreen mode Exit fullscreen mode

The refactored function uses guard clauses instead of nested if-else statements. The variable result is not necessary anymore and has been removed. The refactored code is easier to understand because there is less state (no variable) and each execution path returns early.

How can the original function be refactored step-by-step?

You can simplify the original code by applying two refactorings to each condition:

  • Inline return converts a variable assignment to a return statement. Such a conversion is possible when the following statement after the assignment in the control flow returns the variable.
  • Convert if-else to guard clause changes an if-else statement with return statements inside into to guard clauses, removing the indentation around the else block.

You can carry out these steps manually or with automation. The P42 JavaScript Assistant for VS Code is a refactoring tool that supports both operations. P42 indicates improvement opportunities with blue squiggly underlines. With refactoring automation, the code can be improved in a matter of seconds. After a final formatting step, the function has been refactored into the simplified version.

Here is how refactoring the code looks like in VS Code with P42:

💡  This blog post is showing P42 JavaScript Assistant v1.29

Refactor

Refactoring is essential for maintaining a healthy codebase, and many small improvements go a long way. By introducing guard clauses, you can often simplify nested if-else statements and make your codebase a little bit better.

Happy refactoring!

Top comments (18)

Collapse
 
nemasmisal profile image
nemasmisal
const cases = {
isDead: deadAmount(),
isSeparated: separatedAmount(),
isRetired: retiredAmount()
}
return cases[result] || normalPayAmount()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lgrammel profile image
Lars Grammel • Edited

While I like the object construct and use it in many cases where the values are constant, in this particular case here, it would change the runtime behavior and could lead to unnecessary overhead and bugs. With it, all three functions deadAmount(), separatedAmount(), and retiredAmount() would be called regardless of the actual value of result. Those computations have overhead, could error out, and could have side effects.

Also, in cases where the values are constant and using an object lookup would work, using ?? instead of || for the default value would be even better. If the result for the case is 0, || would trigger because 0 is falsy and normalPayAmount() would be returned, which could be a bug. With ?? this would not happen.

Collapse
 
lgrammel profile image
Lars Grammel

Small bonus: this variant would work without the downsides. However, I think it's harder to understand than the approach with guard clauses / early returns:

const cases = {
  isDead: deadAmount,
  isSeparated: separatedAmount,
  isRetired: retiredAmount
};
return (cases[result] ?? normalPayAmount)();
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rkallan profile image
RRKallan

Using ?? or || depends on expected behaviour.
Your argument could be also the another way. So which is better to use depends on expected behavior

Thread Thread
 
lgrammel profile image
Lars Grammel • Edited

In the original code it was function calls. ?? does not change the behavior in this case, || does if the function returns 0. I'm assuming that the functions return an amount tho, so point taken.

Thread Thread
 
rkallan profile image
RRKallan

if we look to the original code then my assumption isDead, isSeparated & isRetired are booleans.

So in the solution provided as object result needs to be set.

a possible solution could be

function getPayAmount() {
    const cases = {
        deadAmount,
        separatedAmount,
        retiredAmount,
        normalPayAmount,
    };
    let result = "normalPay";

    if (isDead) result = "dead";
    if (isSeparated) result = "separated";
    if (isRetired) result = "retired";

    return cases[`${result}Amount`]();
}

Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
lgrammel profile image
Lars Grammel • Edited

Agreed. Overall this goes back to my point that an object lookup is not the best refactoring here, since there is a risk that it'll change behavior and introduce bugs. Plus it adds unnecessary cognitive complexity.

Btw, I'm a huge fan of using the object lookup pattern when there is a need for it, e.g. when there are many entries or when the entries need to be assembled. In this example tho I think simple if statements are way easier to understand.

Thread Thread
 
rkallan profile image
RRKallan

Agree that if statements with return is easier in this case

Collapse
 
victorocna profile image
Victor Ocnarescu

I have to agree with the author, it's kinda hard to read. Early returns (or guards) are better IMHO as they are simple to write, read and understand.

Collapse
 
kolakola profile image
Jonathan

Yes, is it a better refacto !

Collapse
 
moay profile image
moay • Edited

The term "guard clause" could be somewhat confusing here, because the typical guard clause is not intended to solve the problem of nested logic with different outcomes but to stop the execution of a function in case some condition is not met (so kind of a bouncer or an execution guard).

What you did here is pretty close to the so called "early return pattern" which looks similar and contains guard clauses, but actually solves a different problem and has a wider scope. To read more about early return: medium.com/swlh/return-early-patte...

I am aware of refactoring.com and Martin Fowler, still wanted to point out that in this specific case, most people will probably name the child "early return" instead of "guard".

Collapse
 
lgrammel profile image
Lars Grammel

Good point! I followed the terminology from Fowler's example, which uses guard clause.

Collapse
 
insidiousthedev profile image
Insidious

great post!

Collapse
 
lgrammel profile image
Lars Grammel

Thank you :)

Collapse
 
vineyrawat profile image
Viney Rawat

I was thinking i'm doing wrong but I'm doing it from initial stage. Thankuhh!

Collapse
 
evasteps profile image
Eva Lam

I like it :) just been reading more about the guard clause. Good job !

Collapse
 
lgrammel profile image
Lars Grammel

Thank you 😊

Collapse
 
fk1blow profile image
Dragos Tudorache • Edited

seeing variables that are declared outside, and then accessed inside the function, makes by brain explode