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 valueWhat it is
falsethe boolean itself
0positive zero
-0negative zero, falsy for the same reason
0nthe BigInt zero
''the empty string, in any quote style
nullabsence, assigned deliberately
undefinedabsence, never assigned
NaNthe 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.
  • Infinity and -Infinity are 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.

xx || '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, undefined and NaN. 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. 0 is on the falsy list. ' ' is a one-character string, so truthy. Only emptiness matters for strings, not content.
count is 0. Which expression gives you 0?
count ?? 10 — ?? only substitutes for null and undefined, so it keeps 0. || treats 0 as falsy and returns 10. count && 10 returns 0 too, but for a different reason (&& yields its falsy left operand), and it is not a defaulting expression.

Common mistakes

  • Using || for defaults, which silently discards 0, '' and false.
  • Expecting [] or {} to be falsy, so if (list) is true even when the list is empty. Check list.length.
  • Assuming ?. protects the rest of the chain. a?.b.c still throws when a.b is nullish.

Takeaways

  • Exactly eight values are falsy. Everything else, including [], {} and '0', is truthy.
  • Boolean(x) and !!x are the same conversion; pick one and be consistent.
  • || and && return an operand, not a boolean.
  • Default with ??, so 0, '' and false survive as real values.
  • Optional chaining guards nullish access one link at a time, and pairs naturally with ??.