Why this still matters
Branching is the cheapest way to introduce subtle bugs in any language, and JavaScript adds a few of its own. Loose equality, the unique definition of truthy, the difference between undefined and null, and several decades of habits picked up from other languages all conspire to make if statements harder than they look.
This post brings the basics together with the modern operators (??, ?., logical assignment) that often let you delete a conditional entirely.
If you have not yet read JavaScript variables, scope, and hoisting, start there — it explains the binding rules that conditionals rely on.
Truthy and falsy — the eight values to remember
Every value in JavaScript is either truthy (treated as true in a boolean context) or falsy. There are exactly eight falsy values; everything else is truthy.
| Falsy value | Notes |
|---|---|
false | |
0 | |
-0 | Same as 0 in almost every context. |
0n | The BigInt zero. |
"" (empty string) | ' ' (a space) is truthy. |
null | |
undefined | |
NaN |
Important
Empty arrays
[]and empty objects{}are truthy. This trips up developers coming from Python or Ruby. To test for emptiness usearr.length === 0orObject.keys(obj).length === 0.
if / else if / else
The workhorse. Keep them shallow — nested conditionals are the single biggest readability killer.
1function shippingBand(weightKg) {
2 if (weightKg <= 0) {
3 throw new RangeError("weight must be positive");
4 } else if (weightKg < 1) {
5 return "small";
6 } else if (weightKg < 10) {
7 return "medium";
8 } else {
9 return "large";
10 }
11}
Prefer early return over deep nesting
1// Hard to read
2function process(user) {
3 if (user) {
4 if (user.active) {
5 if (user.email) {
6 sendWelcome(user.email);
7 }
8 }
9 }
10}
11
12// Much clearer
13function process(user) {
14 if (!user) return;
15 if (!user.active) return;
16 if (!user.email) return;
17 sendWelcome(user.email);
18}
The equality operators — always use ===
JavaScript inherited two equality operators: == (loose, performs type coercion) and === (strict, no coercion). The loose form is the source of an entire genre of bugs.
10 == "0"; // true ← surprising
20 == []; // true ← very surprising
3"" == false; // true
4null == undefined; // true ← the one useful loose check
5null === undefined;// false
6
70 === "0"; // false ← always what you want
Note
Rule of thumb: use
===and!==exclusively. The single legitimate use of==isx == null, which istruefor bothnullandundefined— and even that is now better written asx == nullonly if you understand the rule; otherwise spell it out:x === null || x === undefined.
The ternary operator
A pure expression that picks between two values. Excellent for one-line assignments; resist the urge to chain them.
1const label = isPremium ? "Premium" : "Standard";
2
3// Acceptable when each branch is a value
4const fee = country === "US" ? 5
5 : country === "UK" ? 4
6 : 8;
7
8// Almost always better as an if / else
For more than two cases, lift the mapping into a lookup object:
1const fees = { US: 5, UK: 4 };
2const fee = fees[country] ?? 8;
switch — useful, with caveats
switch compares with === and falls through unless you break (or return). Group cases by letting them fall through deliberately:
1function categorise(status) {
2 switch (status) {
3 case "draft":
4 case "review":
5 return "in-progress";
6 case "published":
7 return "done";
8 case "archived":
9 return "done";
10 default:
11 throw new Error(`unknown status: ${status}`);
12 }
13}
Warning
A missing
breakcauses silent fall-through and is one of the classic bug sources. Enable theno-fallthroughESLint rule and use explicitreturnfrom each case where possible.
For value-mapping, a plain object is often a better fit than switch:
1const labels = {
2 draft: "In progress",
3 review: "In progress",
4 published: "Done",
5 archived: "Done",
6};
7const label = labels[status] ?? "Unknown";
Short-circuit evaluation
&& and || do not return booleans — they return one of their operands. That makes them useful for guards and defaults.
1// && — call only if user is truthy
2user && user.profile && user.profile.save();
3
4// || — fall back to a default (CAUTION: zero / empty string also fall back)
5const port = options.port || 3000;
The || default is the source of countless bugs: if options.port is 0 (a perfectly valid port for “any free port”), it is falsy and the default kicks in. Modern JavaScript has a better tool.
Nullish coalescing (??) — the right way to default
Added in ES2020. Returns the right-hand side only when the left is null or undefined — never when it is 0, "", or false.
1const port = options.port ?? 3000; // keeps 0 if you meant it
2const message = options.message ?? "Ready"; // keeps "" if you meant it
3const dark = options.dark ?? false; // keeps false if you meant it
| Source | || | ?? |
|---|---|---|
0 | falls back | keeps 0 |
"" | falls back | keeps "" |
false | falls back | keeps false |
null | falls back | falls back |
undefined | falls back | falls back |
NaN | falls back | keeps NaN |
Optional chaining (?.) — kill the guard pyramid
Also ES2020. Short-circuits to undefined the moment any link in the chain is null or undefined, so you never throw on a missing intermediate property.
1// Before — three nested guards
2const city = user && user.address && user.address.city;
3
4// After
5const city = user?.address?.city;
6
7// Works with function calls and dynamic keys
8const result = api.search?.(query); // call only if search exists
9const firstTag = post?.tags?.[0];
Note
?.()checks the previous link, not the function call’s arguments.obj.fn?.(arg)callsfnonly ifobj.fnis not nullish — butargis still evaluated.
Logical assignment operators
ES2021 added three assignment variants that are surprisingly handy:
1config.timeout ??= 5000; // set only if currently null/undefined
2options.retries ||= 3; // set only if currently falsy
3flags.enabled &&= isReady; // set only if currently truthy
Each is shorthand for x = x ?? y, x = x || y, and x = x && y respectively.
A reusable decision: if, switch, ternary, or table?
| Situation | Best fit |
|---|---|
| Two distinct branches with side effects | if / else |
| Pick one of two values | Ternary |
| Three or more discrete cases on the same variable | switch or lookup |
| Mapping value → value | Lookup object |
| Defaulting a nullable | ?? |
| Defending against a potentially-missing property | ?. |
Summary
- Eight falsy values exist; everything else (including
[]and{}) is truthy. - Use
===and!==. Reserve==for the explicitx == nullidiom — or, better, write the long form. - Replace
||defaults with??unless you specifically want falsy values to be replaced. - Replace nested guards with
?.. - For three or more discrete branches on one variable, prefer a lookup object over a
switch.
The next post introduces JavaScript functions — declarations, expressions, parameters, and arrow functions.










