JAVASCRIPT / VALUES, TYPES, AND COERCION
Truthy and falsy values in conditions
Predict how any value behaves in an if, ternary, or logical operator by recalling JavaScript's eight falsy values and writing the test you actually mean.
What you will learn
- Recite the eight falsy values: false, 0, -0, 0n, empty string, null, undefined, NaN
- Treat every object as truthy, including [], {}, and new Boolean(false)
- Replace if (count) with count !== undefined when 0 is legitimate data
- Pick ?? when 0 and empty strings must survive a default, || when any falsy should not
Understanding Truthy and falsy values in conditions
Every place JavaScript needs a yes-or-no answer runs the value through one internal conversion: the test in if and while, the middle clause of for, the condition before ?, the operand of !, and the deciding operand of && and ||. That conversion is a fixed lookup, not an inspection of the value's contents. Exactly eight values come out false: false, 0, -0, 0n, the empty string, null, undefined, and NaN. Everything else comes out true.
The useful half of that rule is the second half. Because no object appears on the falsy list, every object is truthy: [], {}, function () {}, an Invalid Date, even new Boolean(false). Boolean conversion never calls valueOf or toString and never reads length, so unlike numeric coercion it cannot depend on what an object holds. An empty array is truthy for the same reason a full one is: it is an object, and that is the whole test.
So if (x) literally asks whether x is outside that list of eight, which is the right question only when 0, the empty string, and NaN could never be real data. When they can be, name the condition you mean: x !== undefined for a missing argument, items.length > 0 for an empty array, name.trim() !== "" for a blank field. Boolean(x) and !!x add no behaviour; they just expose the same conversion in a place where you can log it.
const cases = [
["number 0", 0],
["string '0'", "0"],
["empty string", ""],
["null", null],
["NaN", NaN],
["empty array []", []],
["empty object {}", {}],
["string 'false'", "false"],
["new Boolean(false)", new Boolean(false)],
];
for (const [label, value] of cases) {
console.log(label + " -> " + (value ? "truthy" : "falsy"));
}A condition does not ask whether a value is meaningful or non-empty, it asks whether the value is one of the eight falsy values, and every object is truthy.
Worked examples
Zero is data, not absence
Shows how a bare truthiness guard misreports a legitimate zero, and what the guard should have said.
function stockLabel(count) {
if (!count) return "unknown";
return count + " in stock";
}
function stockLabelFixed(count) {
if (count === undefined) return "unknown";
return count + " in stock";
}
console.log(stockLabel(7));
console.log(stockLabel(0));
console.log(stockLabelFixed(7));
console.log(stockLabelFixed(0));Example explained
Line 1!count is true for 0 just as much as for undefined, so a sold-out item reports "unknown".
Line 2stockLabel(7) is correct, which is why this bug survives review: only the zero case is wrong.
Line 3count === undefined tests the single value that means "not supplied", so 0 flows through.
Line 4The last line prints "0 in stock", proving 0 was a usable value all along.
Falsy defaults with || versus nullish defaults with ??
Demonstrates that || falls back on any falsy value while ?? falls back only on null and undefined.
const settings = { volume: 0, theme: "" };
console.log("|| volume:", settings.volume || 50);
console.log("?? volume:", settings.volume ?? 50);
console.log("|| theme:", JSON.stringify(settings.theme || "dark"));
console.log("?? theme:", JSON.stringify(settings.theme ?? "dark"));Example explained
Line 1settings.volume || 50 converts 0 to false and hands back 50, silently unmuting the app.
Line 2?? ignores truthiness and checks only for null or undefined, so the stored 0 survives.
Line 3The theme lines repeat the split for the empty string: || replaces a deliberate blank value, ?? keeps it.
Line 4JSON.stringify is only there so an empty string is visible in the log instead of a blank line.
Emptiness is not falsiness
Shows that empty containers and whitespace strings are truthy, and which expressions actually test emptiness.
const emptyList = [];
if (emptyList) console.log("an empty array is truthy");
console.log(emptyList.length ? "has items" : "no items");
console.log(Object.keys({}).length > 0 ? "has keys" : "no keys");
const blank = " ";
console.log(blank ? "whitespace string is truthy" : "falsy");
console.log(blank.trim() ? "has content" : "only whitespace");Example explained
Line 1if (emptyList) runs because the array is an object, and length plays no part in boolean conversion.
Line 2emptyList.length is 0, a falsy number, so the length is the value worth testing.
Line 3Object.keys({}).length > 0 is the object equivalent, since {} on its own is always truthy.
Line 4" " has characters in it and is therefore truthy, while blank.trim() produces the falsy empty string.
A match at index 0
Illustrates the classic indexOf bug caused by position 0 being falsy.
const tags = ["js", "css"];
if (tags.indexOf("js")) console.log("bare check: js found");
else console.log("bare check: js missing");
if (tags.indexOf("js") !== -1) console.log("explicit check: js found");
console.log(tags.includes("js"));Example explained
Line 1tags.indexOf("js") returns 0 because the match is first, and 0 is falsy.
Line 2The sentinel for "not found" is -1, which is truthy, so the bare check is wrong in both directions.
Line 3Comparing against -1 restores the intended meaning without relying on truthiness.
Line 4includes returns a real boolean, so there is no coercion left to get wrong.
Important notes
In browsers, document.all is the one object that converts to false; it is a deliberate legacy web-compatibility hack, not evidence that objects can be falsy in general.
Truthiness only chooses the branch. && and || still return one of the original operands, so 0 || "" evaluates to "" rather than to false.
Common mistakes
Guarding a numeric value with if (count) or if (!price): a real 0 takes the "missing" branch, so free items show as "price unknown" and zero-length carts show as "cart not loaded".
Testing an array or object for emptiness with if (results): it is always true, so the "no results" branch never runs and the code goes on to touch results[0].name and throws.
Trusting strings read from a form, localStorage, or a URL: "0" and "false" are non-empty strings and therefore truthy, so if (localStorage.getItem("darkMode")) turns dark mode on even when the stored text is "false".
Try it yourself
Change, predict, then run
In a browser console, write a function firstTruthy(list) that returns the first truthy item in an array or the string "none", then call it with [0, "", null, [], 5] and add a comment explaining why the result is the empty array and not 5.
Open the JavaScript workspaceCheck your understanding
if (new Boolean(false)) { console.log("ran"); } prints "ran". What best explains that?
- The Boolean wrapper discards its false argument, so the object stores true.
- Boolean conversion of an object never inspects its contents or calls valueOf, so every object is truthy.
- The condition calls valueOf() on the wrapper, and wrapper objects report true.
- An object is truthy only when it has at least one property, and the wrapper has one.
Show answer
Boolean conversion maps every object to true without running any user code, which is why an empty array and a false-valued wrapper behave identically in a condition. Option 3 is tempting because numeric contexts really do call valueOf, but here it is never consulted, and new Boolean(false).valueOf() is false, so if it were consulted the block would not run at all.