Conditions don't just see true and false
Sooner or later every program has to make a decision: if the user is logged in, show the dashboard. In JavaScript the thing inside the if doesn't have to be a boolean — it can be any value at all, and the language decides whether it counts as "yes" or "no". That decision is called truthiness, and it is one of the most common sources of beginner bugs, because some values feel obvious (0 is no, "hi" is yes) and some trip everyone up (an empty array counts as yes).
Get this wrong and a feature silently misbehaves: a discount that never applies, a default that clobbers a real 0, a form field treated as filled when it is blank. The rules are short and worth memorising, because every condition, every &&, and every || in your code runs on them.
flowchart TD
V["Any value"] --> Q{"Is it one of the<br/>6 falsy values?"}
Q -- Yes --> F["falsy"]
Q -- No --> T["truthy"]
style T fill:#e0900b,color:#fff
The six falsy values
There are exactly six falsy values. Memorise them and everything else is truthy by default: false, 0, "" (the empty string), null, undefined, and NaN.
Notice what is not on that list. The string "0" is truthy because it is a non-empty string — the digit inside is irrelevant. An empty array [] and an empty object {} are both truthy too: any object, even an empty one, counts as yes. This is the trap that catches people who test if (items) to mean "list has entries" — an empty array passes that check every single time.
When you are not sure how a value will behave, the function Boolean(x) tells you directly: Boolean(0) is false, Boolean("0") is true.
console.log(Boolean("0")); // true — non-empty string
console.log(Boolean([])); // true — any object is truthy
console.log(Boolean("")); // false — empty string
console.log(Boolean(0)); // false — zero
const items = [];
console.log(items ? "treated as full" : "empty");
&& and || return operands, not booleans
This is the part that surprises people. && and || do not return true or false. They return one of their two operands, and they stop the moment the answer is certain — that is called short-circuiting.
For ||, read it as "the first truthy value, or the last one if nothing is truthy." "hi" || "there" evaluates "hi", sees it is truthy, and returns it right away. 0 || "fallback" sees 0 is falsy, moves on, and returns "fallback".
For &&, it is the mirror image: "the first falsy value, or the last one if everything is truthy." "hi" && "there" returns "there"; 0 && "x" returns 0.
The practical upshot is that || doubles as a default operator and && doubles as a guard — but both have a sharp edge, which is what comes next.
flowchart LR
O1["a || b"] --> Q1{"a truthy?"}
Q1 -- Yes --> R1["returns a"]
Q1 -- No --> R2["returns b"]
O2["a && b"] --> Q2{"a truthy?"}
Q2 -- Yes --> RR2["returns b"]
Q2 -- No --> RR1["returns a"]
style R1 fill:#1e7f3d,color:#fff
style RR2 fill:#1e7f3d,color:#fff
console.log("hi" || "there"); // "hi" — first truthy wins
console.log(0 || "fallback"); // "fallback" — 0 is falsy
console.log("hi" && "there"); // "there" — all truthy, last returned
console.log(0 && "skipped"); // 0 — first falsy wins
|| as a default, && as a guard
Because || hands back the first truthy operand, it reads naturally as a default: username || "guest" means "use the username, but if it is missing, use guest." Because && hands back the first falsy operand, it reads naturally as a guard: user && user.name means "only reach for the name if there really is a user" — if user is null, the whole expression is null and the dangerous .name lookup never runs.
These two idioms are everywhere in real codebases once you can read them.
Flipping truthiness with !
The ! operator converts a value to its boolean opposite. !value is true when value is falsy and false when it is truthy, because ! first applies the truthiness rule and then negates the result. Apply it twice — !!value — and you get the plain boolean: !!0 is false, !!"hi" is true, !![] is true. That double-bang is the classic shorthand for coercing anything into a clean true or false for storage or comparison.
?? — a default that respects 0 and ""
The nullish coalescing operator ?? is || with the sharp edge filed off. It falls back only when the left side is null or undefined, and leaves every other value — including 0, "", and false — exactly as it is. So 0 ?? 50 is 0, not 50, and "" ?? "anon" keeps the empty string.
Reach for ?? whenever an empty value is legitimate: a real zero count, a genuinely blank field, a false toggle. Reach for || only when every falsy value should trigger the fallback. Most of the time, that distinction is the whole difference between a default that works and one that silently corrupts your data.
const count = 0; console.log(count || 10); // 10 — 0 is falsy, replaced console.log(count ?? 10); // 0 — 0 is NOT null/undefined, kept const name = ""; console.log(name || "anon"); // "anon" console.log(name ?? "anon"); // "" kept
flowchart TD
V["You want a default for value v"] --> Q{"Is 0 or '' a<br/>valid value for v?"}
Q -- "Yes" --> HH["use v ?? fallback"]
Q -- "No" --> OO["use v || fallback"]
style HH fill:#1e7f3d,color:#fff
style OO fill:#b45309,color:#fff
What does this print? || falls back for any falsy value; ?? falls back only for null or undefined.
console.log(0 || "default"); console.log(0 ?? "default");
0 is falsy, so || moves on to its right side.
0 is not null or undefined, so ?? keeps it.
Write displayName(name) that returns name when it is set, and "Anonymous" only when name is null or undefined. An empty string is a valid name and must be kept.
function displayName(name) {
// return name, or "Anonymous" only for null/undefined
}
Use the nullish coalescing operator ?? .
?? only replaces null and undefined, so "" is preserved.
function displayName(name) {
return name ?? "Anonymous";
}
Write firstAvailable(a, b, c) that returns the first truthy argument among a, b, and c — or "none" if all three are falsy. Chain || to do it.
function firstAvailable(a, b, c) {
// return the first truthy of a, b, c, or "none"
}
Chain the three with ||, then || "none" at the end.
|| returns the first truthy value it finds, or the last value if none are truthy.
function firstAvailable(a, b, c) {
return a || b || c || "none";
}
This should apply a default page count of 50 only when count is null or undefined. Instead it uses ||, so a real count of 0 is thrown away and replaced by 50. Change the one operator so pages(0) returns 0 while pages(null) still returns 50.
function pages(count) {
return count || 50;
}
|| replaces every falsy value, including 0.
?? only replaces null and undefined, so a real 0 survives.
function pages(count) {
return count ?? 50;
}
Write summarise(items) that returns the string "empty" when items is null, undefined, OR an empty array [], and otherwise returns the number of items. Use logical operators, not if. The catch: an empty array [] is truthy, so items || "empty" alone will not work — you need its length.
function summarise(items) {
// return "empty" for null/undefined/[], else items.length
}
null && anything short-circuits to null (falsy).
[] && [].length gives 0 because [] is truthy, so && returns the right side, 0.
A non-empty array's length is truthy, so || returns that number.
function summarise(items) {
return (items && items.length) || "empty";
}
Recap
- Exactly six values are falsy:
false,0,"",null,undefined,NaN. Everything else — including"0",[], and{}— is truthy. &&and||short-circuit and return one of their operands, not a boolean.||gives the first truthy value;&&gives the first falsy one.||makes a handy default, but it swallows0and""along withnullandundefined.??makes a default that only replacesnullandundefined, so real zeroes and empty strings survive.- Test an unknown value with
Boolean(x)(or!!x) when you are unsure how it will behave in a condition.
Next you'll put these conditions to work building real logic — and the defaults you just learned are what keep that logic honest when data is missing.