Stop using nested ifs: Do this instead

Typical use case for nested ifs: you want to perform all sorts of checks on some data to make sure it’s valid before finally doing something useful with it.

Don’t do this! πŸ‘‡

JavaScript
function sendMoney(account, amount) { if (account.balance > amount) { if (amount > 0) { if (account.sender === 'user-token') { account.balance -= amount; console.log('Transfer completed'); } else { console.log('Forbidden user'); } } else { console.log('Invalid transfer amount'); } } else { console.log('Insufficient funds'); } }

There’s a better way:

JavaScript
function sendMoney(account, amount) { if (account.balance < amount) { console.log('Insufficient funds'); return; } if (amount <= 0) { console.log('Invalid transfer amount'); return; } if (account.sender !== 'user-token') { console.log('Forbidden user'); return; } account.balance -= amount; console.log('Transfer completed'); }

See how much cleaner it is? Instead of nesting ifs, we have multiple if statements that do a check and return immediately if the condition wasn’t met. In this pattern, we can call each of the if statements a guard clause.

If you do a lot of Node.js, you’ve probably seen this flow in Express middleware:

JavaScript
function authMiddleware(req, res, next) { const authToken = req.headers.authorization; if (!authToken) { return res.status(401).json({ error: 'Unauthorized' }); } if (authToken !== 'secret-token') { return res.status(401).json({ error: 'Invalid token' }); } if (req.query.admin === 'true') { req.isAdmin = true; } next(); }

It’s much better than this, right? :

JavaScript
function authMiddleware(req, res, next) => { const authToken = req.headers.authorization; if (authToken) { if (authToken === 'secret-token') { if (req.query.admin === 'true') { req.isAdmin = true; } return next(); } else { return res.status(401).json({ error: 'Invalid token' }); } } else { return res.status(401).json({ error: 'Unauthorized' }); } };

You never go beyond one level of nesting. We can avoid the mess that we see in callback hell.

How to convert nested ifs to guard clauses

The logic for this for doing this is simple:

1. Find the innermost/success if

Here we can clearly see it’s the cond3 if. After this, if we don’t do any more checks and take the action we’ve always wanted to take.

JavaScript
function func(cond1, cond2, cond3) { if (cond1) { if (cond2) { if (cond3) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 3'); } } else { console.log('failed condition 2'); } } else { console.log('failed condition 1'); } }

2. Invert the outermost if and return

Negate the if condition to put the else statements’ body in there and add a return after.

Delete the else braces (keep the body, it still contains the formerly nested ifs, and move the closing if brace to just after the return.

So:

JavaScript
function func(cond1, cond2, cond3) { if (!cond1) { // πŸ‘ˆ inverted if condition // πŸ‘‡ body of former else clause console.log('failed condition 1'); return; // πŸ‘ˆ exit on fail } // πŸ‘‡ remaining nested ifs to convert to guard clauses if (cond2) { if (cond3) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 3'); } } else { console.log('failed condition 2'); } }

3. Do the same for each nested if until you reach the success if

And then:

JavaScript
function func(cond1, cond2, cond3) { if (!cond1) { console.log('failed condition 1'); return; } if (!cond2) { console.log('failed condition 2'); return; } // πŸ‘‡ remaining nested ifs to convert if (cond3) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 3'); } }

And finally:

JavaScript
function func(cond1, cond2, cond3) { if (!cond1) { console.log('failed condition 1'); return; } if (!cond2) { console.log('failed condition 2'); return; } if (!cond3) { console.log('failed condition 3'); return; } console.log('PASSED!'); console.log('taking success action...'); }

I use the JavaScript Booster extension to make inverting if statements in VS Code much easier.

Here we only had to put the cursor in the if keyword and activate the Show Code Actions command (Ctrl + . by default).

Check out this article for an awesome list of VSCode extensions you should definitely install alongside with JavaScript Booster.

Tip: Split guard clauses into multiple functions and always avoid if/else

What if we want to do something other after checking the data in an if/else? For instance:

JavaScript
function func(cond1, cond2) { if (cond1) { if (cond2) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 2'); } console.log('after cond2 check'); } else { console.log('failed condition 1'); } console.log('after cond1 check'); }

In this function regardless of cond1‘s value, the 'after cond1 check' the line will still print. Similar thing for the cond2 value if cond1 is true.

In this case, it takes a bit more work to use guard clauses:

If we try to use guard clauses, we’ll end up repeating the lines that come after the if/else checks:

JavaScript
function func(cond1, cond2) { if (!cond1) { console.log('failed condition 1'); console.log('after cond1 check'); return; } if (!cond2) { console.log('failed condition 2'); console.log('after cond2 check'); console.log('after cond1 check'); return; } console.log('PASSED!'); console.log('taking success action...'); console.log('after cond2 check'); console.log('after cond1 check'); } func(true);

Because the lines must be printed, we print them in the guard clause before returning. And then, we print it in all(!) the following guard clauses. And once again, in the main function body if all the guard clauses were passed.

So what can we do about this? How can we use guard clauses and still stick to the DRY principle?

Well, we split the logic into multiple functions:

JavaScript
function func(cond1, cond2) { checkCond1(cond1, cond2); console.log('after cond1 check'); } function checkCond1(cond1, cond2) { if (!cond1) { console.log('failed condition 1'); return; } checkCond2(cond2); console.log('after cond2 check'); } function checkCond2(cond2) { if (!cond2) { console.log('failed condition 2'); return; } console.log('PASSED!'); console.log('taking success action...'); }

Let’s apply this to the Express middleware we saw earlier:

JavaScript
function authMiddleware(req, res, next) { checkAuthValidTokenAdmin(req, res, next); } function checkAuthValidTokenAdmin(req, res, next) { const authToken = req.headers.authorization; if (!authToken) { return res.status(401).json({ error: 'Unauthorized' }); } checkValidTokenAdmin(req, res, next); } function checkValidTokenAdmin(req, res, next) { const authToken = req.headers.authorization; if (authToken !== 'secret-token') { return res.status(401).json({ error: 'Invalid token' }); } checkAdmin(req, res, next); } function checkAdmin(req, res, next) { if (req.query.admin === 'true') { req.isAdmin = true; } next(); }

In a way, we’ve replaced the if/else statements with a chain of responsibility pattern. Of course, this might be an overkill for simple logic like a basic Express request middleware, but the advantage here is that it delegates each additional check to a separate function, separating responsibilities and preventing excess nesting.

Key takeaways

Using nested ifs in code often leads to complex and hard-to-maintain code; Instead, we can use guard clauses to make our code more readable and linear.

We can apply guard clauses to different scenarios and split them into multiple functions to avoid repetition and split responsibilities. By adopting this pattern, we end up writing cleaner and more maintainable code.



Every Crazy Thing JavaScript Does

A captivating guide to the subtle caveats and lesser-known parts of JavaScript.

Every Crazy Thing JavaScript Does

Sign up and receive a free copy immediately.

Leave a Comment

Your email address will not be published. Required fields are marked *