Table of Contents
Why Rust Has No NULL (And C Still Does)#
"Segmentation fault. Core dumped."
If you write C or C++, these four words have probably ended one of your days at some point. The crash almost always means the same thing: your code tried to access a memory address that does not exist. And in the overwhelming majority of cases, the culprit is a single, tiny thing — the NULL pointer.
In 1965, a computer scientist named Tony Hoare introduced the null reference into a programming language. Decades later, he publicly apologized, calling it his "Billion Dollar Mistake" — his estimate of the total cost of the crashes and bugs it has caused since.
So if NULL is that dangerous, why does C still use it? And how did Rust manage to eliminate it completely, without giving up low-level control? Let's look under the hood.
In C, NULL is a pointer to nowhere#
In C, a pointer is fundamentally just an integer holding a memory address. Assigning a pointer to NULL points it at a very specific address: zero.
int *ptr = NULL; // (void*)0 — address zero
*ptr = 42; // read/write to address zero
Here is the problem: modern operating systems deliberately leave the zero page of memory unmapped. The moment your code tries to read or write through a NULL pointer, the CPU raises a hardware exception. The operating system steps in and kills the process instantly. That is your segmentation fault.
The compiler does not care. It assumes you know what you are doing, so it trusts you. The only protection C offers is a convention — a check you must remember to write yourself, every single time:
if (ptr != NULL) {
printf("%d\n", *ptr);
}
The check is optional. Forget it once, on one pointer, in one rarely-executed branch, and the whole program dies at 3 a.m. in production.
Rust's answer: a box for the maybe-missing#
Rust takes a fundamentally different approach: it simply has no NULL.
Instead, Rust expresses "this value might not exist" with a type called Option<T>. If a value might be missing, Rust forces you to put it inside a box. The box has exactly two possible states:
let config: Option<String> = read_config_file();
// Some(path) — the data is there
// None — the box is empty
So far it looks like a fancier NULL. The difference is what the compiler does when you try to use the value:
let config: Option<String> = read_config_file();
// let path = config; ← does NOT compile: Option<String> is not a String
if let Some(path) = config {
println!("config path: {path}");
} else {
println!("no config file found — using defaults");
}
In C, checking for NULL is optional. In Rust, handling None is mandatory. The compiler refuses to compile code that reaches inside the box without first proving the box is not empty — usually with a match or if let. You cannot forget, because forgetting is a compile-time error.
The Billion Dollar Mistake is caught before your code ever runs.
Safety with zero extra bytes#
At this point a C programmer will ask the obvious question: if Rust wraps pointers in an Option box, does that safety cost memory? Does every pointer now carry an extra flag for "Some" versus "None"?
This is where Rust's compiler shows off. Look at the actual memory layout:
use std::mem::size_of;
println!("{}", size_of::<*mut i32>()); // 8 (a raw C-style pointer)
println!("{}", size_of::<Option<Box<i32>>>()); // 8 (the safe version)
On a 64-bit system both are exactly 8 bytes. But how does Option distinguish Some from None without storing an extra flag anywhere?
Because Rust knows one fact that C never uses: a valid pointer can never be address zero. So the compiler takes the one impossible bit pattern — 0x0 — and silently uses it to represent None. The remaining 2^64 − 1 patterns all mean Some. This is the null pointer optimization, and it generalizes to references, Box, Vec, and many other types.
At the machine code level, Option<Box<i32>> looks exactly like a C null pointer. At the syntax level, the compiler makes it impossible to dereference blindly. The safety is real, and the runtime cost is exactly zero.
The verdict#
C leaves null-checking entirely in the hands of the programmer. It is fast — and it is a minefield, because humans forget.
Rust deletes NULL from the language and replaces it with Option<T>: the compiler enforces the check at compile time, and the null pointer optimization ensures pointers do not grow by a single byte.
That is the fix for the Billion Dollar Mistake: not a better convention, but a type system that makes the mistake unrepresentable.
