Truthiness and Defaults
Mental model: Eight values are falsy. Everything else is truthy, including the empty containers people expect to be falsy.
Level: beginner · about 12 minutes
if ('0') console.log("'0' is truthy");
if ([]) console.log('[] is truthy');
if ({}) console.log('{} is truthy');
if (!'') console.log("'' is falsy");
if (!0) console.log('0 is falsy');Three of these lines surprise most beginners.
Anywhere JavaScript needs a boolean (an if, a while, !, &&, ||, a ternary) it runs ToBoolean on the value. That operation has no logic to it beyond a list: eight values convert to false, and everything else in the language converts to true.
The complete list of falsy values
| Falsy value | What it is |
|---|---|
false | the boolean itself |
0 | positive zero |
-0 | negative zero, falsy for the same reason |
0n | the BigInt zero |
'' | the empty string, in any quote style |
null | absence, assigned deliberately |
undefined | absence, never assigned |
NaN | the not-a-number number |
[]is truthy. An empty array is still an object.{}is truthy, for the same reason.'0'is truthy. It is a one-character string.'false'is truthy. So is'null'and' '.function () {}is truthy, always.Infinityand-Infinityare truthy, and so is any non-zero number.new Boolean(false)is truthy, because it is an object. Never use it.
console.log(Boolean('false'), !!'false'); // true true
console.log(Boolean([]), !![]); // true true
console.log(Boolean(' '), !!' '); // true true, whitespace is not empty
console.log(Boolean(0), !!0); // false false
console.log(Boolean(NaN), !!NaN); // false false
`Boolean(x)`- the explicit conversion. Prefer it in code that others read.
`!!x`- the same conversion twice-negated. Idiomatic and fine, once you recognise it.
`if (x)`- the implicit version. Perfect when you genuinely mean "any truthy value".
Short-circuiting returns a value, not a boolean
console.log(0 || 'fallback'); // 'fallback' || yields the first truthy operand
console.log('set' || 'fallback'); // 'set'
console.log(1 && 'second'); // 'second' && yields the last, if all are truthy
console.log(null && 'never'); // null the right side never runs
const user = null;
console.log(user && user.name); // null, and no TypeError
?? versus ||, the practical difference
Wrong: || eats valid values
const volume = 0;
const label = '';
const debug = false;
console.log(volume || 50); // 50
console.log(label || 'x'); // 'x'
console.log(debug || true); // true
Right: ?? only fills in nullish
const volume = 0;
const label = '';
const debug = false;
console.log(volume ?? 50); // 0
console.log(label ?? 'x'); // ''
console.log(debug ?? true); // false|| asks "is this falsy?". ?? asks "is this null or undefined?". For user settings, counts, prices, coordinates and flags, the second question is the one you meant. Reach for || only when every falsy value really should be replaced.
x | x || 'd' | x ?? 'd' |
|---|---|---|
undefined | 'd' | 'd' |
null | 'd' | 'd' |
0 | 'd' | 0 |
'' | 'd' | '' |
false | 'd' | false |
NaN | 'd' | NaN |
'ok' | 'ok' | 'ok' |
Optional chaining: ?., ?.(), ?.[]
const config = { server: { port: 8080 }, tags: ['a'] };
console.log(config.server?.port); // 8080
console.log(config.client?.port); // undefined, no TypeError
console.log(config.tags?.[0]); // 'a'
console.log(config.onReady?.()); // undefined, the call is skipped entirely
console.log(config.client?.port ?? 3000); // 3000, the natural pairing
is it missing? -> x == null or x ?? fallback is it empty or zero? -> explicit check: x.length === 0, x === 0 is it anything at all? -> if (x) (falsy list applies) is it exactly false? -> x === false
Guard clauses keep the happy path flat
function priceLabel(item) {
if (item == null) return 'no item';
if (item.price == null) return 'price unknown'; // 0 is a real price
if (item.price === 0) return 'free';
return `${item.price} USD`;
}
console.log(priceLabel(null), priceLabel({}), priceLabel({ price: 0 }), priceLabel({ price: 9 }));
const settings = { volume: 0, label: '', retries: null };
console.log(settings.volume || 50, settings.volume ?? 50);
console.log(settings.label || 'none', settings.retries ?? 3);0 is falsy, so || replaces it with 50, while ?? keeps it because 0 is not nullish. '' is falsy too, so || gives 'none'. retries is null, which is nullish, so ?? gives 3. This one prediction is the whole lesson.
Try it yourself
Truthy or falsy?
const candidates = [false, 0, -0, 0n, '', null, undefined, NaN, [], {}, '0', 'false', ' '];
for (const value of candidates) {
const label = typeof value === 'string' ? JSON.stringify(value) : String(value);
console.log(label.padEnd(12), Boolean(value) ? 'truthy' : 'falsy');
}
console.log('falsy count:', candidates.filter((v) => !v).length, 'of', candidates.length);
Add candidates of your own: ' ', [[]], 0n, -0, new Boolean(false), Infinity.
Exercises
A defaultTo that respects 0, '' and false
Write defaultTo(value, fallback) that returns fallback only when value is null or undefined. Every other value, including 0, '', false, NaN and 0n, must be returned unchanged.
Implement isEmpty
Write isEmpty(value). It is true for null, undefined, a string that is empty or only whitespace, an array with no elements, a Map or Set with no entries, and an object with no own keys. It is false for everything else, including 0, false and NaN, because those are values, not absences.
Check yourself
- How many falsy values does JavaScript have?
- 8 — Eight:
false,0,-0,0n,'',null,undefinedandNaN. Everything else is truthy, including[],{},'0'and'false'. - What does this log?
- true true false true —
'false'is a five-character string, so truthy.[]is an object, so truthy.0is on the falsy list.' 'is a one-character string, so truthy. Only emptiness matters for strings, not content. countis0. Which expression gives you0?count ?? 10—??only substitutes fornullandundefined, so it keeps0.||treats0as falsy and returns10.count && 10returns0too, but for a different reason (&&yields its falsy left operand), and it is not a defaulting expression.
Common mistakes
- Using
||for defaults, which silently discards0,''andfalse. - Expecting
[]or{}to be falsy, soif (list)is true even when the list is empty. Checklist.length. - Assuming
?.protects the rest of the chain.a?.b.cstill throws whena.bis nullish.
Takeaways
- Exactly eight values are falsy. Everything else, including
[],{}and'0', is truthy. Boolean(x)and!!xare the same conversion; pick one and be consistent.||and&&return an operand, not a boolean.- Default with
??, so0,''andfalsesurvive as real values. - Optional chaining guards nullish access one link at a time, and pairs naturally with
??.