Look at this line:
let user = null;
Enter fullscreen mode Exit fullscreen mode
There’s something quietly strange about it. You have a variable. You have a value assigned to it. And that value is being used to express the idea that there is no value.
A missing value is being represented as a value. That contradiction is where the problem starts.
What a Reference Actually Is
In most languages, when you create an object, your variable doesn’t hold the object directly. It holds a reference, something that points to where the object lives in memory.
variable
↓
reference
↓
object in memory
Enter fullscreen mode Exit fullscreen mode
This is why multiple variables can refer to the same object. They don’t contain separate copies of the object; they hold references to the same underlying thing in memory.
When a variable is null, that reference points to nothing. The variable exists. The reference exists. But there’s no object at the other end.
variable
↓
null
↓
(nothing)
Enter fullscreen mode Exit fullscreen mode
What null actually is under the hood depends on the language and runtime. In many systems, a null reference is represented as a zero address or some other sentinel value, but that’s an implementation detail. The important thing conceptually is that the reference isn’t pointing anywhere meaningful.
Now do this:
user.name
Enter fullscreen mode Exit fullscreen mode
If user is null, there’s nothing to follow. The program tries to dereference a reference that leads nowhere. Depending on the language, this produces a null reference error, a null pointer exception, a crash, or some other runtime failure. The exact behaviour differs, but the cause is the same: you tried to use a reference that led nowhere.
Why Hoare Called It a Mistake
Tony Hoare introduced null references in ALGOL W in 1965. He’s also the person who invented Quicksort, so he’s not someone who made careless decisions. Decades later he called it his “billion-dollar mistake,” characterising the enormous cumulative cost that null-related errors had caused across the software industry.
His reasoning wasn’t that null itself is conceptually wrong. It was that introducing null adds an implicit state to every reference in the language.
Before null, a variable of type User means: there is a User here.
After null, a variable of type User means: there is a User here, or there isn’t.
Every function that accepts a User now has two possible inputs instead of one. Every caller now has to decide whether to check. Every place that doesn’t check is a latent bug. The type said User, but it was quietly lying.
How Null Propagates
The real cost isn’t the crash. It’s how far absence can travel before anyone notices.
Imagine a chain like this:
user.address.city.name
Enter fullscreen mode Exit fullscreen mode
If address can be null, this line might fail. But the actual mistake might not be here. The mistake might be several layers back, in a database query that returned nothing, which got assigned to a variable typed as User, which got passed through two functions that didn’t check, which eventually reached this line.
The crash happens here. The bug lives somewhere else entirely.
Once null is possible, that possibility has to be tracked everywhere the value travels. Functions that receive a nullable value have to handle absence. Functions that call those functions have to know. The chain looks something like this:
Function A → might return null
Function B → has to check
Function C → checks again
Function D → forgets
Enter fullscreen mode Exit fullscreen mode
At scale, this becomes exhausting. Not because any single null check is hard, but because the information that a value might be absent has to stay accurate across every function, every layer, every developer who touches the code. The compiler isn’t tracking it. Discipline is.
What Modern Languages Do Instead
Kotlin handles this by making nullability part of the type.
val name: String // cannot be null
val name: String? // might be null
Enter fullscreen mode Exit fullscreen mode
A String and a String? are different types. If you try to call a method on a String? without handling the null case, the compiler complains. You can use the safe call operator:
name?.length
Enter fullscreen mode Exit fullscreen mode
which returns null instead of crashing if name is null. Or you can do an explicit check. The point is that the type now communicates whether absence is possible, and the compiler helps enforce that you’ve dealt with it.
Rust goes further with Option<T>:
Option<String>
Enter fullscreen mode Exit fullscreen mode
Rather than a null value, absence is represented explicitly through the type. A value is either Some(value) or None. There’s no String that secretly might be nothing. If a function might not return a value, its return type says so explicitly. And if you want to use the value inside, you have to unwrap it, which means you have to handle the None case.
Some(value) → here's the value
None → there is no value
Enter fullscreen mode Exit fullscreen mode
This doesn’t eliminate all bugs related to missing data. You can still write incorrect logic. But the possibility of absence is encoded in the type, not hidden behind a convention that everyone has to remember.
The deeper problem with null was never just that it causes crashes. It’s that it introduces an implicit state that the type system doesn’t track. A value that could secretly be nothing is a value the whole program has to silently handle everywhere.
When Hoare called it a billion-dollar mistake, he wasn’t saying the concept of absence is wrong. Absence is real. Data can be missing. The question is whether that possibility should be invisible or explicit.
A missing value shouldn’t pretend to be just another value. When it does, you don’t have one kind of thing in your program. You have two. And only one of them is written in the types.