Stop writing nested if statements
One small change made my code way easier to read and maintain. Instead of nesting conditionals deeper and deeper, handle edge cases first and let the happy path speak for itself.
The problem nobody talks about
Nested if statements don't feel like a problem when you're writing them. You're in flow, the logic makes sense in your head, and the code works. The problem shows up three weeks later when someone else — or future you — has to change it.
Here's what deeply nested code actually costs you:
- Readability — you have to scan multiple levels of indentation before you understand what the function does
- Mental overhead — you're holding the state of every outer condition in your head as you read inward
- Hidden bugs — edge cases buried inside nested branches are easy to miss in code review
- Painful debugging — when something fails, you have to mentally unwind the nesting to trace the execution path
The deeper the nesting, the worse all of these get. And it compounds — nested ifs attract more nested ifs.
What I used to write
The pattern shows up constantly in any code that validates preconditions before running logic. A fintech example makes it obvious:
/**
* ❌ BEFORE -- this is what I used to do
*/
// check if user exists
if (user) {
// check if user has KYC
if (user.hasKyc) {
// check if user above 18
if (user.age > 18) {
// do something
}
}
}Three conditions. Three levels of nesting. And we haven't even written the actual logic yet — that goes inside the innermost block. Every new precondition pushes the real work one level deeper.
This is the arrow anti-pattern. Your code points right instead of running down, and the indentation itself becomes noise that obscures intent.
Guard clauses
The fix is to invert each condition and return early — fail fast, exit immediately, and only reach the core logic once every precondition has been cleared.
/**
* ✅ AFTER -- Now, this is what I do
*/
// check if user doesn't exists
if (!user) {
return 'Not logged in';
}
// check if user doesn't have KYC
if (!user.hasKyc) {
return 'Need KYC verification';
}
// check if user < 18
if (user.age < 18) {
return 'User underage';
}
// do that "thing" which needed all those checksSame logic. Same three conditions. But now the code reads like a checklist — top to bottom, one thing at a time. Each guard is self-contained: it checks one thing, returns a clear reason if it fails, and gets out of the way.
By the time execution reaches the bottom, every precondition has passed. The reader doesn't have to hold anything in their head.
Why this matters more than it looks
The mechanical change is small — flip the condition, add a return. The cognitive change is significant.
Each failure case is isolated. In the nested version, adding a new check means deciding where in the nesting hierarchy it belongs. In the guard version, you add one if block at the top. No restructuring, no risk of breaking adjacent logic.
The happy path is obvious. With guards, the real logic lives at the bottom, unindented, with nothing in its way. A reader scanning the function can immediately see what it's actually supposed to do — it's the code that isn't a guard.
Code review gets easier. Reviewers can validate each guard independently. There's no need to mentally simulate nested conditionals to verify that the right cases are handled.
Error messages become explicit. In the nested version, a failed inner condition produces nothing — the function just silently doesn't execute. With guard clauses, every failure path has a return value that tells you exactly why.
When it matters most
Guard clauses shine in any function that validates state before doing real work: auth checks, input validation, feature flags, rate limiting, permission checks. Anywhere you're answering "should this function even run?" before answering "what should it do?"
The pattern also scales. Three guards is clean. Ten guards is still readable — it's just a longer checklist. Ten levels of nesting is unworkable.
The rule I follow now
Handle edge cases first → then run the real logic.
Clean code is not about fewer lines. It's about less thinking required to understand it. Your future teammates — and your future self — will thank you.
Try it in your next PR. It's a one-line refactor per condition, and code reviews get noticeably smoother when the structure isn't fighting the reader.