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! π
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:
// β
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:
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? :
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.
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 if
s, and move the closing if
brace to just after the return
.
So:
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:
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:
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.
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:
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:
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:
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:
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.