Control flow is how a program decides what to do next. Without it, code runs top-to-bottom with no decisions, no branches, no reactions to different situations. The if statement is the most fundamental control flow tool in JavaScript โ it evaluates a condition and runs a block of code only when that condition is true. In this lesson you will master if, else, else if, nested conditions, and the modern guard clause pattern that keeps code clean and readable.
if / else / else if Syntax
| Form | Syntax | When It Runs |
|---|---|---|
| Single if | if (condition) { ... } |
Only when condition is truthy |
| if / else | if (c) { ... } else { ... } |
One branch always runs |
| if / else if / else | if (c1) {} else if (c2) {} else {} |
First truthy branch runs; else is fallback |
| Nested if | if (a) { if (b) { ... } } |
Both conditions must be true |
| Ternary | condition ? valueA : valueB |
Inline expression โ not a statement |
Condition Evaluation Rules
| Condition Type | Example | Evaluates As |
|---|---|---|
| Strict equality | age === 18 |
true only when age is exactly number 18 |
| Comparison | score >= 90 |
true when score is 90 or higher |
| Logical AND | age >= 18 && hasId |
true only when both are true |
| Logical OR | isAdmin || isMod |
true when either is true |
| Negation | !isLoggedIn |
true when isLoggedIn is falsy |
| Nullish check | user != null |
true when user is neither null nor undefined |
Guard Clause vs Nested if
| Style | Approach | Readability |
|---|---|---|
| Nested (avoid) | Happy path wraps deeper and deeper inside ifs | Pyramid of doom โ hard to follow |
| Guard clause (prefer) | Return or throw early for invalid conditions; happy path at the bottom | Flat, easy to read top-to-bottom |
false, 0, '', null, undefined, and NaN. Everything else โ including [], {}, and '0' โ is truthy. This means if (user) is a valid and common way to check whether a user object exists.if (valid) { ... }, return early: if (!valid) return;. The rest of the function then only runs for valid input โ no indentation pyramid, no mental overhead tracking which closing brace belongs to which if.{ } with if statements, even for single-line bodies. The braceless form if (x) doThing(); is legal but dangerous โ adding a second line to the “block” without braces will not be part of the condition, causing silent bugs that are very hard to spot.Basic Example
// โโ Basic if / else if / else โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function getGrade(score) {
if (score >= 90) {
return 'A';
} else if (score >= 80) {
return 'B';
} else if (score >= 70) {
return 'C';
} else if (score >= 60) {
return 'D';
} else {
return 'F';
}
}
console.log(getGrade(95)); // 'A'
console.log(getGrade(82)); // 'B'
console.log(getGrade(55)); // 'F'
// โโ Logical AND / OR in conditions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function canAccessDashboard(user) {
if (user !== null && user.isActive && (user.role === 'admin' || user.role === 'editor')) {
return true;
}
return false;
}
console.log(canAccessDashboard({ isActive: true, role: 'admin' })); // true
console.log(canAccessDashboard({ isActive: false, role: 'admin' })); // false
console.log(canAccessDashboard({ isActive: true, role: 'viewer' })); // false
console.log(canAccessDashboard(null)); // false
// โโ Guard clause pattern โ flat and readable โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function processOrder(order) {
// Guard: reject invalid input early
if (!order) { return { error: 'No order provided' }; }
if (!order.items?.length) { return { error: 'Order has no items' }; }
if (order.total <= 0) { return { error: 'Invalid order total' }; }
if (!order.userId) { return { error: 'No user ID on order' }; }
// Happy path โ all guards passed, safe to proceed
const tax = order.total * 0.08;
const shipping = order.total >= 35 ? 0 : 5.99;
const grand = order.total + tax + shipping;
return {
orderId: `ORD-${Date.now()}`,
subtotal: order.total,
tax: tax.toFixed(2),
shipping: shipping.toFixed(2),
total: grand.toFixed(2),
};
}
console.log(processOrder(null));
// { error: 'No order provided' }
console.log(processOrder({ items: ['widget'], total: 29.99, userId: 'u42' }));
// { orderId: 'ORD-...', subtotal: 29.99, tax: '2.40', shipping: '5.99', total: '38.38' }
// โโ Ternary for simple inline decisions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const cartCount = 3;
const cartLabel = cartCount === 1 ? '1 item' : `${cartCount} items`;
const freeShip = cartCount >= 2 ? 'Free shipping!' : 'Add more for free shipping';
console.log(cartLabel); // '3 items'
console.log(freeShip); // 'Free shipping!'
// โโ Nullish-safe conditional โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function greetUser(user) {
if (user?.name) {
console.log(`Welcome back, ${user.name}!`);
} else {
console.log('Welcome, Guest!');
}
}
greetUser({ name: 'Alice' }); // Welcome back, Alice!
greetUser(null); // Welcome, Guest!
greetUser({}); // Welcome, Guest!
How It Works
Step 1 โ JavaScript Evaluates the Condition to a Boolean
The expression inside if ( ... ) is coerced to true or false. Complex conditions using && and || short-circuit โ && stops at the first falsy value, || stops at the first truthy value. This means user !== null && user.isActive never accesses user.isActive when user is null.
Step 2 โ else if Chains Are Evaluated Top to Bottom
JavaScript tests each else if in order and runs the first branch whose condition is true, then skips all remaining branches. In getGrade, a score of 85 matches score >= 80 and returns 'B' immediately โ it never evaluates score >= 70.
Step 3 โ Guard Clauses Invert the Logic
Instead of if (valid) { doWork() }, guard clauses check for invalid conditions and return early. The function body after the guards only executes when all preconditions are met. This eliminates nesting and makes the success path obvious โ it is always the last block at the deepest indentation level.
Step 4 โ Optional Chaining Inside Conditions
user?.name returns undefined (falsy) if user is null or undefined, rather than throwing a TypeError. Combined with an if statement, this safely handles missing data without needing a separate null check first.
Step 5 โ Ternary Is an Expression, Not a Statement
Unlike if, the ternary operator produces a value โ it can be used directly in assignments, function arguments, and template literals. Keep ternaries simple: one condition, two short outcomes. For anything more complex, use a regular if/else block for readability.
Real-World Example: User Permission System
// permissions.js
const ROLES = {
ADMIN: 'admin',
EDITOR: 'editor',
VIEWER: 'viewer',
GUEST: 'guest',
};
function getUserPermissions(user) {
// Guard clauses
if (!user) { return { canRead: false, canWrite: false, canDelete: false }; }
if (!user.isActive) { return { canRead: false, canWrite: false, canDelete: false }; }
const role = user.role ?? ROLES.GUEST;
if (role === ROLES.ADMIN) {
return { canRead: true, canWrite: true, canDelete: true };
} else if (role === ROLES.EDITOR) {
return { canRead: true, canWrite: true, canDelete: false };
} else if (role === ROLES.VIEWER) {
return { canRead: true, canWrite: false, canDelete: false };
} else {
return { canRead: false, canWrite: false, canDelete: false };
}
}
function renderUI(user) {
const perms = getUserPermissions(user);
const editBtn = perms.canWrite ? '<button>Edit</button>' : '';
const deleteBtn = perms.canDelete ? '<button>Delete</button>' : '';
const content = perms.canRead ? '<article>Content...</article>' : '<p>Access denied</p>';
return `${content}${editBtn}${deleteBtn}`;
}
// Tests
const admin = { role: 'admin', isActive: true };
const editor = { role: 'editor', isActive: true };
const banned = { role: 'admin', isActive: false };
console.log(getUserPermissions(admin)); // { canRead: true, canWrite: true, canDelete: true }
console.log(getUserPermissions(editor)); // { canRead: true, canWrite: true, canDelete: false }
console.log(getUserPermissions(banned)); // { canRead: false, canWrite: false, canDelete: false }
console.log(getUserPermissions(null)); // { canRead: false, canWrite: false, canDelete: false }
Common Mistakes
Mistake 1 โ Assignment instead of comparison inside if
โ Wrong โ single = assigns and always evaluates truthy:
if (user = getUser()) { } // assigns getUser() to user โ always true if getUser returns anything
โ Correct โ use === for comparison:
if (user === getUser()) { } // compares โ or simply: if (getUser()) { }
Mistake 2 โ Missing braces causing silent logic bugs
โ Wrong โ second line is NOT inside the if block:
if (isAdmin)
showPanel();
deleteButton.show(); // always runs โ NOT part of the if!
โ Correct โ always use braces:
if (isAdmin) {
showPanel();
deleteButton.show(); // now correctly inside the if block
}
Mistake 3 โ Deep nesting instead of guard clauses
โ Wrong โ pyramid of doom, hard to read:
function process(data) {
if (data) {
if (data.user) {
if (data.user.isActive) {
// actual work buried 3 levels deep
}
}
}
}
โ Correct โ guard clauses, flat and clear:
function process(data) {
if (!data) return;
if (!data.user) return;
if (!data.user.isActive) return;
// actual work here โ no nesting
}
Quick Reference
| Pattern | Syntax | Best For |
|---|---|---|
| Simple branch | if (x) { } else { } |
Two possible outcomes |
| Multiple branches | if (a) {} else if (b) {} else {} |
3+ mutually exclusive outcomes |
| Guard clause | if (!valid) return; |
Early exit for invalid input |
| Ternary | x ? a : b |
Simple inline value selection |
| Nullish-safe | if (obj?.prop) { } |
Condition on possibly-null object |
| Combined condition | if (a && b && c) { } |
All must be true |