DEV Community

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

JS Refactoring Combo: Replace Nested If-Else with Guards

Lars Grammel on September 29, 2021

Nested if-else statements can make it unnecessarily complicated to reason about the different executions paths and results of a function. A loss of...
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