A user profile has a middle name field. It can be absent because the user has no middle name, absent because they were never asked, or absent because they cleared it. Three different facts, and if your code stores all three as the same empty value, it can never tell them apart again. Every question you later ask — how many users have we asked, how many answered, how many said no — becomes unanswerable.
This is not a language quirk. It is a modelling decision that most codebases make by accident, usually at the moment someone writes a default. The languages differ in what tools they give you, but the underlying distinction is the same everywhere.
Below: what the three absences actually mean, the specific place they get flattened into one, and how to decide which you want.
Three absences, three meanings
The first is "no value exists" — a deliberate, recorded absence. The user was asked, and the answer is that there is nothing. This is a fact, and it is usually what a null in a database column is for: the row exists, the field is knowingly empty.
The second is "not set" — nobody has determined anything. The question was never asked, the object was never given that property, the response did not include the field. This is the absence of a fact rather than a fact about absence. Some languages give it a distinct value; others cannot express it separately at all.
The third is a real value that happens to be empty: an empty string, a zero, an empty list. The user was asked, answered, and their answer was nothing-shaped. This is not absence at all, though it is the one most often conflated with the other two, because emptiness reads as absence to code that only checks truthiness.
The line where they get flattened
The flattening almost always happens at a default. Code reads a field, finds nothing, and substitutes an empty string so the rest of the function is simpler. From that line onward, "never asked" and "asked, answered nothing" are indistinguishable — the information was not lost by the database or the network, it was discarded by a convenience.
The second place is a truthiness check. In most languages, a check for "is this present" also rejects zero, the empty string, and sometimes an empty collection. So a quantity legitimately set to zero, or a note deliberately cleared, takes the same branch as a field that was never populated. This is how a valid zero becomes a missing value, and it is one of the most common sources of quietly wrong numbers.
Both mistakes share a shape: they are locally sensible and globally destructive. Nothing about the line where the information is lost hints that anything upstream cared about the difference, which is why review rarely catches it.
Choosing on purpose
The question to ask of every optional field is whether you will ever need to distinguish "we do not know" from "there is nothing". If the answer is yes — and for anything a user answers, anything with an audit trail, anything you will report on — then the two states need different representations all the way through, from the column to the API to the type.
Where the answer is genuinely no, collapsing them is fine and simpler, and the important thing is to do it deliberately and write down that you did. A comment saying "empty and unset are the same here because nothing distinguishes them for us" is worth more than the ambiguity it replaces, and it tells the next person that the collapse was a decision rather than an accident.
For presence checks, test for the specific absence rather than for falsiness. Most languages offer a way to ask "is this null or unset" that does not also catch zero and the empty string. Using it costs nothing and removes the whole class of valid-zero bugs.
Where the language offers it, let the type system carry the distinction instead of a convention. An optional type that must be unwrapped before use turns "I forgot this could be missing" from a runtime surprise into a compile error, and it documents the intent at the same time. That is a far stronger guarantee than a naming habit, because it holds for the person who joins next year and never read this.
At the boundaries, say it explicitly
The distinction is easiest to lose where data crosses a boundary. In JSON, a field that is absent and a field present with a null value are different, and many clients treat them identically. For an update endpoint this matters a great deal: absent should usually mean "leave it alone" and null should mean "clear it", and a client that cannot express the difference cannot ask you to clear a field at all.
A related trap is the API that omits a field it has no value for. A client reading that response cannot tell whether the server does not know, the server knows there is nothing, or the field was dropped by a version mismatch — three quite different situations collapsed into one absence at the exact boundary where a schema could have separated them. Serialising null explicitly costs a few bytes and keeps the distinction alive across the wire.
Databases make the opposite mistake available. A NOT NULL column with an empty-string default looks tidy and quietly guarantees that unset and empty are the same forever, with no migration able to recover which rows were which. Allowing null there is not untidiness; it is preserving a distinction you may need.
The rule that generalises: at every boundary, decide what absence means and encode it, rather than letting a default decide for you. Absence is information, and it is the kind that cannot be reconstructed once it has been rounded off.
Why JavaScript's specific choice to have both null and undefined is a genuine design outlier
Most mainstream languages settle on a single representation for 'no value here,' while JavaScript deliberately distinguishes `undefined`, a variable that was declared but never assigned, or a property that was never set at all, from `null`, an explicit, deliberate assignment representing 'intentionally no value' — this distinction is a real, if sometimes debated, design choice rather than an accident, and treating the two as interchangeable, rather than understanding the specific semantic difference each one is meant to signal, is a common source of subtle bugs in exactly this language.
Why an empty string, an empty array, and a genuinely absent value need three genuinely different checks
Code that checks only `if (value)` to guard against absence treats an empty string, the number zero, and an empty array as equally falsy alongside actual `null` or `undefined`, which is frequently not the intended behavior at all — a form field left blank is meaningfully different from a form field that was never rendered in the first place, and collapsing both into the same falsy check produces logic that behaves correctly for some inputs and silently wrong for others, purely because the check was broader than the actual distinction the code needed to make.
Why the nullish coalescing operator was added specifically to fix a common, real mistake with the logical OR operator
Using `||` to supply a default value — `count || 10` — incorrectly falls back to the default even when `count` is legitimately zero, since zero is falsy, which is exactly the kind of bug that motivated adding the `??` nullish coalescing operator, which falls back to a default only for genuine `null` or `undefined`, correctly preserving a legitimate zero, empty string, or `false` value rather than incorrectly overwriting it — knowing when to reach for `??` instead of `||` specifically prevents this common, easily overlooked class of default-value bug.
Why database NULL and a language's own null or undefined are related but not identical concepts
A relational database's own `NULL` represents 'unknown or not applicable' with its own distinct three-valued logic — a comparison against `NULL` returns neither true nor false but a third, genuinely separate 'unknown' result — while a programming language's `null` or `undefined` is typically just an ordinary value that participates in ordinary boolean logic; conflating the two, assuming an ORM's mapping between database `NULL` and a language's own null value preserves every subtlety of SQL's three-valued logic, is a real, if less commonly discussed, source of subtle bugs at exactly the boundary between application code and the database.
Optional chaining and default values done right
The optional chaining operator, written as a question mark before a dot, was added to JavaScript specifically to shorten the extremely common pattern of guarding against null or undefined at every step of a property access chain. Instead of writing a manual check like user && user.address && user.address.city, optional chaining lets you write user?.address?.city and get undefined automatically the moment any link in that chain is missing, without throwing.
Pairing optional chaining with the nullish coalescing operator gives a clean, correct way to reach into possibly-missing data and supply a default only when the value is truly absent: user?.address?.city ?? 'Unknown' returns the city if the whole chain resolves, and falls back to the default only if something along the way was null or undefined — not if the city happens to be an empty string, which a real city name never is but which the old `||` pattern would have wrongly overridden.
Designing your own APIs to avoid the ambiguity
The deepest fix for this whole category of confusion is architectural: when you are the one designing a function's return value or an object's shape, you get to decide whether absence is represented at all, and if so, by exactly one value rather than several. A function that returns null on 'not found' and undefined on 'not yet loaded' and an empty array on 'loaded, zero results' is handing its caller three different states to check for, each easy to conflate with the others under time pressure.
The more disciplined approach is to pick one convention for the whole codebase — commonly: undefined means the value was never set, and everything else, including null and empty collections, is treated as a legitimate present value — and document it once so every function respects it. Consistency here matters more than which specific convention is chosen, because the actual cost is paid whenever a caller has to guess which absence value a particular function might return.
The cost of getting this wrong in a public API
When this ambiguity leaks into a function or endpoint that other teams or external developers consume, the cost multiplies, because every caller now has to independently discover and remember which absence value your API actually returns. Documenting the convention explicitly in the function's type signature or API schema, rather than leaving callers to find out by trial and error, is a small effort that prevents a large number of downstream bugs.