JAVASCRIPT / CONDITIONALS
Guard clauses and flattening nested checks
Flatten deeply nested if blocks into guard clauses that return, continue, or throw early, so the main path of a function reads at one indentation level.
What you will learn
- Turn `if (ok) { work(); }` at the end of a function into `if (!ok) return; work();`
- Guard loop bodies with `continue` instead of wrapping them in an if block
- Order guards existence, then shape, then rules, so later lines cannot throw
- Spot when an early return is unsafe: trailing statements would be skipped
Understanding Guard clauses and flattening nested checks
Nesting costs you memory, not lines. Every `if` that wraps the rest of a function adds a condition you must keep holding while you read downward, so by the time you reach the interesting line you are mentally tracking "the order exists, and it has items, and the country is NL". A guard clause inverts the check and leaves the function immediately, which lets you discard the condition instead of carrying it: once `if (!order) return 'no order';` has run, `order` is simply not something you think about again.
The rewrite is mechanical. When an `if`/`else` is the last thing in a function, take the outer condition, negate it, return whatever the `else` branch returned, and unindent the rest. That equivalence depends on the branch really being the tail of the function; if a statement after the block has to run for every input, an early return silently skips it, and you must move that statement into the guard or pull the tail into its own function. Inside a loop the same move uses `continue`, and for input that is genuinely broken rather than merely uninteresting, `throw` plays the same role.
Guards create facts. Each one narrows the set of states the following lines can be in, which is why their order is part of the logic and not a style preference: existence first, because reading a property of `null` throws instead of quietly evaluating to `false`, then shape, then business rules. When two guards could both match the same input, the earlier one decides the result, so a banned owner answering "account banned" instead of "ok" is something you choose by placing that line above the ownership test.
function shipNested(order) {
if (order) {
if (order.items.length > 0) {
if (order.country === 'NL') {
return order.items.length * 4;
} else {
return 'no shipping to ' + order.country;
}
} else {
return 'empty order';
}
} else {
return 'no order';
}
}
function shipGuarded(order) {
if (!order) return 'no order';
if (order.items.length === 0) return 'empty order';
if (order.country !== 'NL') return 'no shipping to ' + order.country;
return order.items.length * 4;
}
const cases = [
null,
{ items: [], country: 'NL' },
{ items: ['pen', 'ink'], country: 'DE' },
{ items: ['pen', 'ink', 'pad'], country: 'NL' }
];
for (const order of cases) {
console.log(shipNested(order) + ' | ' + shipGuarded(order));
}A guard clause converts a wrapping condition into an immediate exit, so the real work sits at one indentation level with every earlier condition already guaranteed.
Worked examples
continue as a loop guard
Shows the loop equivalent of an early return, keeping the useful statement at one indentation level.
const rows = [
{ name: 'ada', score: 91 },
null,
{ name: 'grace', score: -4 },
{ name: 'alan', score: 84 },
{ name: 'bob', score: 61 }
];
const passed = [];
for (const row of rows) {
if (!row) continue;
if (row.score < 0) continue;
if (row.score < 80) continue;
passed.push(row.name);
}
console.log(passed.join(', '));Example explained
Line 1`if (!row) continue;` abandons the current iteration, so `row.score` below is only ever read on a real object.
Line 2The `-4` row is dropped by the second guard, which means the third guard never has to reason about impossible scores.
Line 3`passed.push(row.name)` sits at one level of indentation even though three conditions had to pass to reach it.
Line 4grace and bob are filtered out by guards rather than by nested ifs, leaving `ada, alan`.
return inside forEach is not a guard
Demonstrates why a guard written in a callback fails to exit the enclosing function.
const names = ['', 'ada', 'grace'];
function firstNameBroken(list) {
list.forEach(function (n) {
if (n === '') return;
return n;
});
return 'none';
}
function firstName(list) {
for (const n of list) {
if (n === '') continue;
return n;
}
return 'none';
}
console.log(firstNameBroken(names));
console.log(firstName(names));Example explained
Line 1`return n` inside the `forEach` callback ends only that one call; `forEach` throws the value away and keeps iterating.
Line 2So `firstNameBroken` always reaches `return 'none'`, even though `'ada'` matched on the second element.
Line 3In the `for...of` version the guard is `continue` and the exit is `return n`, and both act on `firstName` itself.
Line 4Guards only work where the construct you are in can actually be exited.
guard order decides the answer
Shows existence guards first, a compound condition negated correctly, and how ordering picks which failure wins.
function canEdit(user, doc) {
if (!user) return 'sign in first';
if (!doc) return 'no such document';
if (user.banned) return 'account banned';
if (doc.locked) return 'document locked';
if (doc.ownerId !== user.id && user.role !== 'admin') return 'not your document';
return 'ok';
}
console.log(canEdit(null, { ownerId: 7 }));
console.log(canEdit({ id: 3, banned: true }, { ownerId: 3 }));
console.log(canEdit({ id: 7 }, { ownerId: 7, locked: true }));
console.log(canEdit({ id: 2, role: 'admin' }, { ownerId: 7 }));
console.log(canEdit({ id: 9 }, { ownerId: 7 }));Example explained
Line 1The first two guards establish that `user` and `doc` exist, so every line below can read properties without a null check.
Line 2`doc.ownerId !== user.id && user.role !== 'admin'` is the negation of "owner or admin": flattening a compound rule flips `||` into `&&`.
Line 3The banned user owns the document, so without the `user.banned` guard above the ownership test that call would have answered `ok`.
Line 4`return 'ok'` is reached only when no guard matched, which is what makes the last line readable on its own.
Important notes
Negating a compound condition is where flattening usually breaks: the guard for `if (a && b)` is `if (!a || !b) return;`, not `if (!a && !b) return;`.
Guards replace conditions that end the function; if the code after them still needs a genuine two-way choice, an `if`/`else` at that point is the honest thing to write.
Common mistakes
Writing the condition without an exit, as in `if (!user) 'no user';` or a guard whose `return` was forgotten: execution falls through and the next line throws `TypeError: Cannot read properties of null`.
Using `return` inside a `forEach` callback as a guard for the outer function: it ends only that callback call, so the loop finishes and the function returns its fallback value instead.
Placing a property guard above the existence guard, such as `if (order.items.length === 0)` before `if (!order)`, which converts a clean 'no order' result into a crash on `null`.
Try it yourself
Change, predict, then run
In a browser console, write `describeUser(user)` using three nested ifs (user exists, `user.name` is a non-empty string, `user.age >= 18`) with a different string returned for each failing case. Then rewrite it with guard clauses and confirm both versions print the same result for `null`, `{}`, `{ name: 'ada', age: 12 }` and `{ name: 'ada', age: 30 }`.
Open the JavaScript workspaceCheck your understanding
In which situation does replacing a wrapping `if (ready) { ... }` with the guard `if (!ready) return;` change what the function does?
- When statements after the if block still need to run for every input
- When the if block contains more than one statement
- When `ready` is the result of a function call instead of a variable
- When the function is written as an arrow function instead of a declaration
Show answer
An early return abandons the entire rest of the function body, so any trailing statement such as a log, a counter increment, or a cleanup call is skipped for the guarded inputs, and the rewrite is no longer equivalent. Having several statements inside the block is tempting to pick but changes nothing: flattening only shifts those statements out one level, and they still run in the same order under the same condition.