Three values in JavaScript stand in for "nothing": false, null and undefined. Only two of them are strictly booleans, and the difference between null and undefined is a question of intent rather than mechanics — which is exactly why beginners get them backwards so often.
Which values are falsy?
Every value in JavaScript is either truthy or falsy when evaluated in a condition. The falsy set is short and completely fixed: false, 0, -0, 0n, the empty string, null, undefined and NaN. Everything else is truthy.
const values = {
false: false,
zero: 0,
emptyString: "",
null: null,
undefined: undefined,
NaN: NaN,
emptyArray: [],
emptyObject: {},
zeroString: "0",
};
for (const [name, value] of Object.entries(values)) {
console.log(`${name.padEnd(13)} ${value ? "truthy" : "falsy"}`);
}
The output lists the first six as falsy and the last three as truthy. The three that surprise people are at the bottom: an empty array is truthy, an empty object is truthy, and the string "0" is truthy. JavaScript does not ask whether a container has contents — it asks whether the value is a particular set of things, and arrays and objects are not on that list.
That is also why arr.length === 0 is the correct emptiness test, and why if (arr) is a bug waiting to happen: an empty array passes the test even though there is nothing in it.
What is the difference between null and undefined?
undefined means "no value was provided". null means "a value was provided, and it is deliberately empty". One is an absence the language creates for you; the other is a decision you make.
let missing; // no value assigned
const explicitlyEmpty = null; // deliberately empty
console.log(missing); // undefined
console.log(explicitlyEmpty); // null
console.log(typeof missing); // "undefined"
console.log(typeof explicitlyEmpty); // "object" ← yes, really
Two things fall out of that. First, typeof undefined is "undefined" while typeof null is "object" — a bug from 1995 that could not be fixed without breaking the web, and which the type-checking lesson in this module unpacks properly.
Second, the two compare differently. null == undefined is true, but null === undefined is false. The strict comparison is false because they are genuinely different values: one means empty, the other means missing.
How do I check for a missing value safely?
With ??, the nullish coalescing operator. It returns the right-hand side only when the left-hand side is null or undefined — and nothing else.
const port = 0;
const retries = 3;
const note = "";
console.log(port || 3000); // 3000 ← wrong: 0 is falsy
console.log(port ?? 3000); // 0 ← right: 0 is a real value
console.log(retries ?? 5); // 3
console.log(note || "none"); // "none"
console.log(note ?? "none"); // "" ← an empty string is still a value
The first and second lines are the whole argument for ??. port || 3000 returns 3000 because 0 is falsy, even though 0 is a perfectly meaningful port number. port ?? 3000 returns 0, because 0 is neither null nor undefined.
Use || when a falsy value genuinely should fall back — an empty field that should become a placeholder. Use ?? whenever the fallback should apply only to a genuinely missing value. When in doubt, ?? is the safer default.
There is a related operator for reaching into nested data without crashing: optional chaining, ?.. It short-circuits to undefined when the value in front of it is null or undefined, so user?.address?.city gives you undefined instead of a TypeError. It has its own lesson later in this tutorial.
How do default values work with missing arguments?
A function parameter with a default only kicks in when the argument is undefined. That is precise and useful: it means a caller who explicitly passes null is not accidentally overwritten.
function greet(name) {
return `Hello, ${name ?? "friend"}!`;
}
console.log(greet("Ada")); // "Hello, Ada!"
console.log(greet()); // "Hello, friend!"
console.log(greet(null)); // "Hello, friend!"
The second and third calls produce the same output, because ?? treats both undefined and null as missing. If you need only undefined to count as missing, a default parameter does that instead — but default parameters are covered in full in the functions module of this tutorial.
What happens to undefined in JSON?
JSON.stringify drops any object property whose value is undefined, but keeps the ones that are null. That difference matters whenever you send data to a server or store it.
const record = { id: 7, name: "Ada", nickname: undefined, score: null };
console.log(JSON.stringify(record));
console.log(Object.keys(record));
The first line prints {"id":7,"name":"Ada","score":null} — the nickname key is gone, the score key is still there with a null. The second line prints [ 'id', 'name', 'nickname', 'score' ], because the property still exists in the object even though JSON will not carry it across a wire.
That gap causes real bugs. If a server expects {"nickname": null} to mean "no nickname set" and your payload omits the key entirely, the server may treat the missing key as "the client did not send this field" and apply a default. If you want an explicit empty value, send null, not undefined.
How do these values show up in real input?
Form input is a useful place to see the difference in practice. A text field always gives you a string. An empty text box gives you "" — not null, and not undefined. That has a direct consequence for validation: a check written as if (!value) treats the string "0" as empty, which is wrong for a field where zero is a legitimate answer.
const input = { name: "", discount: "0" };
console.log("name looks empty:", !input.name); // true
console.log("discount looks empty:", !input.discount); // false — "0" is a non-empty string
console.log("discount as a number:", Number(input.discount)); // 0
So the rule for form fields is: check the trimmed string for emptiness explicitly, and convert to a number before testing the number. A common, safe pattern is to trim first, then compare against an empty string, then convert:
const raw = " ";
const cleaned = raw.trim();
console.log("raw length:", raw.length); // 2
console.log("cleaned length:", cleaned.length); // 0
console.log("is empty:", cleaned === ""); // true
Notice that " " is not empty — it has two characters — but it almost certainly should be treated as empty by a form. That is what trim() is for, and it is the difference between a validation rule that surprises users and one that does not.
The distinctions worth remembering
- Falsy means one of eight things:
false,0,-0,0n,"",null,undefined,NaN. Empty arrays and objects are truthy. undefinedmeans nothing was provided.nullmeans something was provided and it is deliberately empty.null === undefinedisfalse. Use??to treat them alike, not==.- Prefer
??over||whenever 0, an empty string orfalseis a legitimate value. JSON.stringifydropsundefinedproperties and keepsnullones. Sendnullwhen you mean "empty".
Try it yourself: write a function describe(value) that returns one of three strings — "missing" for null and undefined, "empty" for "" and 0, and "present" for everything else. Test it with describe(), describe(null), describe(0), describe("") and describe([]). The last one is the interesting one: an empty array is "present", even though it holds nothing, and now you know exactly why.