Taming long and nested if/else statements
By: Rajesh Naroth
Tags: JavaScript, Clean Code, Web Development, Programming, React

Taming long and nested if/else statements

Image by Steve Buissinne from Pixabay

Case 1. Simple if / else if / else blocks

if (mode === "A") {

return { isDisabled: true };

} else if (mode === "B") {

return { isDisabled: true };

} else {

return { isDisabled: false };

}

This is a common pattern that we see in applications. However, it is ripe for future spaghetti like updates. Let us see how we can tame it.

Option 1. Lose the else

We will try removing the else keyword. It is in fact, redundant.

if (mode === "A") {

return { isDisabled: true };

}

if (mode === "B") {

return { isDisabled: true };

}

return { isDisabled: false };

Option 2. Use a switch statement

The previous solution is a little less cluttered but why not use a switch statement here?

switch(mode) {

case "A":

return { isDisabled: true };

case "B":

return { isDisabled: true };

default:

return { isDisabled: false };

}

Both if and switch blocks has more or less the same code density. Switch statements are notorious for unintended case fall throughs and forgotten defaults.

Option 3. What about a ternary?

return mode === "A"

? { isDisabled: true }

: mode === "B"

? { isDisabled: true }

: { isDisabled: false };

Ternaries may seem alien if you are not used to them but with proper formatting (use Prettier), it will read much easier, they are much less wordy than imperative if/else statements. Yet, the object pattern may be the best approach here.

Option 4: The object pattern

const status = {

["A"]: { isDisabled: true },

["B"]: { isDisabled: true },

}

return status[mode] || { isDisabled: false };

Much better! Adding a new case should be as easy as adding a new map within the object. However, we cannot substitute nested expressions with the object pattern, let us take a look at another example.

Case 2. Nested if else blocks

const getMessage = (selected, mode, action) => {

if (selected.length === 0) {

return "Empty List";

}

if (selected.length > 1 && action !== "edit") {

return "Multi edit";

}

if (selected.accessible && mode === "A") {

if (action === "edit") {

return "Multi select A Edit";

}

return "Multi select A View";

}

return "No OP";

}

During code-reviews, this style is considered a code smell. This innocent little function will grow into a monster soon with more conditions slapped on as the application ages. It is also hard to unit test since the number of combinations are numerous; by its nature, no one will understand it fully to update any existing tests. If we’re lucky, the author would include some comments.

We can’t just create an object model here. How about ternaries.

const getMessage = (selected, mode, action) => {

return (selected.length === 0) {

? "Empty List"

: (selected.length > 1 && action !== "edit")

? "Multi edit"

: (selected.accessible && mode === "A")

? (action === "edit")

? "Multi select A Edit"

: "Multi select A View"

: "No OP";

}

Nested ternaries may be less wordy but are confusing, to me it is an unreadable blob of code, the mental gymnastics it takes to decipher it is not trivial. Code should be self documenting as much as possible and here is what I recommend:

const emptyListMessage = (selected) =>

selected.length === 0 ? "Empty List" : null; const multiSelectEditMessage = (selected, action) =>

selected.length > 1 && action !== "edit" ? "Multi edit" : null; const multiSelectModeAMessage = (action) =>

action === "edit" ? "Multi select A Edit" : "Multi select A View"; const accessibleModeMessage = (selected, mode, action) =>

selected.accessible && mode === "A"

? multiSelectModeAMessage(action)

: null; const defaultMessage = () => "No OP"; const getMessage = (selected, mode, action) =>

emptyListMessage(selected)

|| multiSelectEditMessage(selected, action)

|| accessibleModeMessage(selected, mode, action)

|| defaultMessage();

Our code now is now mostly functions and expressions, the very essence of functional programming, they have a mathematical quality; once properly unit tested they tend to hold strong and become resilient to bugs. Here, we’ve almost doubled the code surface yet, it reads quite well. Each part of the logic is self documented which is quite useful where the conditions are complex. We can unit test these smaller functions much easier and they will sometimes even get reused. To me, the biggest challenge here is in naming these so that they communicate the intent effectively.

Tip: If you have complex conditional expressions, move them to their own function. Applying this to our example above, we get a function like this, a sweet little one liner that can be simply unit tested.