Conditionals
Mental model: A statement picks a path; an expression produces a value. Choose the one that matches what you actually need.
Level: beginner · about 10 minutes
const score = 82;
if (score >= 90) {
console.log('A');
} else if (score >= 80) {
console.log('B'); // → 'B'
} else {
console.log('C or below');
}Run this, then change the score to 55 and predict the output before running again.
An if statement runs a block when its condition is truthy. else if chains are checked top to bottom and stop at the first match, which is why the order of your branches is part of the logic, not a style choice.
The condition is coerced to a boolean, so if (list.length) and if (name) work. That is convenient and occasionally a trap: 0, '', NaN, null and undefined are all falsy, so if (count) skips a real count of zero.
function describe(count) {
if (count) return 'has items'; // 0 falls through
return 'empty';
}
console.log(describe(3)); // → 'has items'
console.log(describe(0)); // → 'empty' ... a real zero, treated as missing
// Say what you mean instead:
const better = (count) => (count > 0 ? 'has items' : 'empty');
console.log(better(0)); // → 'empty', and now it is deliberate
The ternary is an expression
An if statement cannot be assigned to anything. A ternary can, because it evaluates to a value. That single difference decides which one to reach for.
Statement: assign in both branches
let label;
if (isAdmin) {
label = 'Admin';
} else {
label = 'Member';
}
Expression: one const
const label = isAdmin
? 'Admin'
: 'Member';The ternary version needs no let and no reassignment, so a reader can see at a glance that label never changes again. Use a ternary when you want a value, and an if when you want an action.
Early return beats nesting
Nested: read down and right
function pay(order) {
if (order) {
if (order.items.length) {
if (order.paid) {
return 'ship it';
}
}
}
return 'wait';
}
Early return: read down only
function pay(order) {
if (!order) return 'wait';
if (!order.items.length) return 'wait';
if (!order.paid) return 'wait';
return 'ship it';
}Handle the reasons to stop first, one per line, then write the happy path at the end with zero indentation. Each guard is now a sentence you can read on its own.
switch compares with ===
function planFor(role) {
switch (role) {
case 'owner':
case 'admin': // deliberate fallthrough, both get the same body
return 'full access';
case 'member':
return 'read and write';
default:
return 'read only';
}
}
console.log(planFor('admin'), planFor('member'), planFor('ghost'));Note the deliberate grouping: two labels share one body.
const id = '2';
switch (id) {
case 2:
console.log('number two');
break;
default:
console.log('no match');
}switch uses strict equality, so '2' === 2 is false and the only branch left is default. This is the single most common switch bug, and it usually arrives via input.value, which is always a string. Convert first with Number(id).
When the switch is really a table
A long switch where every branch just returns a value is a lookup table written the slow way. Replace it with an object or a Map and the data becomes something you can iterate, count and test.
const LABELS = new Map([
[200, 'OK'],
[301, 'Moved Permanently'],
[404, 'Not Found'],
[500, 'Server Error'],
]);
const labelFor = (code) => LABELS.get(code) ?? 'Unknown status';
console.log(labelFor(404)); // → 'Not Found'
console.log(labelFor(418)); // → 'Unknown status'
console.log(labelFor('toString')); // → 'Unknown status' (a plain object would leak here)
| You want | Reach for | Because |
|---|---|---|
| To do something in one case | if | no value is produced |
| To choose between two values | ternary | it is an expression, so const works |
| To reject bad input | early return guards | flat, one reason per line |
| Three or more branches on one value | switch or a lookup | the value is compared once |
| A branch per key, returning data | object or Map lookup | data beats code you have to read |
Try it yourself
Flatten the nesting
function checkout(order) {
if (order) {
if (order.items && order.items.length > 0) {
if (order.paid) {
return 'ship';
} else {
return 'awaiting payment';
}
} else {
return 'empty cart';
}
} else {
return 'no order';
}
}
console.log(checkout(null));
console.log(checkout({ items: [] }));
console.log(checkout({ items: ['book'], paid: false }));
console.log(checkout({ items: ['book'], paid: true }));
Rewrite checkout using early returns so nothing is indented more than one level. Then add a rule: an order over 1000 needs order.approved.
Switch versus lookup
function symbolSwitch(currency) {
switch (currency) {
case 'USD': return '$';
case 'EUR': return '€';
case 'GBP': return '£';
case 'JPY': return '¥';
default: return '?';
}
}
const SYMBOLS = { USD: '$', EUR: '€', GBP: '£', JPY: '¥' };
const symbolLookup = (currency) =>
Object.hasOwn(SYMBOLS, currency) ? SYMBOLS[currency] : '?';
console.log(symbolSwitch('EUR'), symbolLookup('EUR'));
console.log(symbolSwitch('XYZ'), symbolLookup('XYZ'));
console.log(Object.keys(SYMBOLS).length, 'currencies, countable without reading any code');
Add a fifth currency to both versions and notice which edit you would rather make on a Friday afternoon.
Exercises
FizzBuzz, but as a function
Write fizzBuzz(n). Return 'Fizz' when n divides by 3, 'Buzz' when it divides by 5, 'FizzBuzz' when it divides by both, and otherwise the number as a string. Order your branches so the both case cannot be swallowed.
Replace the switch with a lookup
Rewrite httpLabel(code) without a switch. Use a Map of numeric codes to labels and return 'Unknown status' for anything missing. Keys are numbers, so the string '200' must not match, and neither must 'toString'.
Check yourself
- What does this log?
- 'high' — The chain stops at the first truthy condition, and
500 >= 10matches immediately, so'very high'is unreachable. Overlapping branches must go from narrowest to widest. - Why can you write
const label = a ? b : cbut notconst label = if (a) b else c? - A ternary is an expression and produces a value;
ifis a statement and does not — Expressions evaluate to a value you can assign, pass or return. Statements perform an action. That is why a ternary fits inside a template literal or a JSX attribute and anifdoes not. - You have twelve branches that each return a different string for a known key. What is usually the best shape?
- An object or
Maplookup with a fallback — Branches that only map a key to a value are data. Stored as a lookup they can be counted, iterated, validated and tested, and adding the thirteenth entry is a one-line edit rather than a code change.
Common mistakes
- Using
if (count)when zero is a legitimate value. Compare explicitly withcount > 0. - Ordering
else ifbranches from widest to narrowest, so the specific case never runs. - Forgetting that
switchuses===, then wondering why a numericcasenever matches a value read from an input.
Takeaways
else ifchains stop at the first match, so branch order is part of the behaviour.- A ternary produces a value; an
ifperforms an action. Pick by what you need. - Guard clauses turn nesting into a flat list of reasons to stop early.
switchcompares strictly, and a returning-onlyswitchis usually a lookup in disguise.