Null is not ok
Your continued
null
usage is the secondhand smoke of programming
Using null
today is like smoking in the late 60’s. Experts agree it’s bad for you and the data backing up that claim is irrefutable. Most people when really pressed will admit that they intuitively know it’s bad, but will go on to say it’s not a big deal or they like it anyway. The majority of people still use it in spite of the obvious dangers.
The analogy goes deeper than that. A big part of the public health campaign against smoking was to address the reality that it wasn’t, outside your private home, an individual choice. By smoking in public spaces you exposed others to the dangers of smoking without their consent.
I contend that continued null
usage is the secondhand smoke of programming and that programmers should feel ashamed. That might seem a bit much, but remember: some amount of shame is good. As Bill Maher once put it:
Shame is the first step of reform, it’s what goads people into saying “maybe I can do better”.
We shamed people out of smoking, and we can shame them out of using null
too.
But that’s just like, your opinion man
I do sincerely mean everything you will read here even if I would otherwise phrase it more gently one on one.
This isn’t a subjective matter. We can appeal to experts, to data / historical precedence, and reason a priori on why nulls are bad. First though, let’s define null
:
null
ornullptr
ornil
is an exception to the type system. We claim that we have aFoo
, or a pointer to aFoo
, or a reference to aFoo
, but really we might have one or might have nothing.
The keyword used to represent this concept isn’t relevant. As we’ll see below, the keyword null
is still used in systems that I’ll call legitimate.
Bad in theory
But hold on, if we’ve defined the type to mean Foo
or nothing, why is that wrong? Particularly in languages where object instances are always references it’s well understood they might be null
. Types are arbitrary, what grounds could you possibly be objecting on?
This line of reasoning fails in theory and in practice.
You may recall learning about the Reflexive Property in your geometry or algebra classes. If you were like me, it was a weird thing to spend any time on because it’s obvious. In basic classes, you’re not introduced to number theory or postulates so it’s easy for the importance to be overlooked. Funny enough, this defense of null
demonstrates why it is important and you can’t take it for granted!
Reflexive Property states that: A = A
Compare that to:
1
2
var foo = new Foo();
var foo = null;
I use var
instead of Foo
to lay bare what might otherwise be hidden by status quo bias. These aren’t the same types. The first one is definitely a Foo
, the second might be one.
1
2
3
4
5
var foo = new Foo();
var bar = null;
foo = bar; // Valid
bar = new Bar();
foo = bar; // Not valid!
Null isn’t just another value of a reference, it’s a magic, typeless value. In this world, A != A
instead A = A || null
which clearly isn’t going to yield a consistent system1. This is why many call null an exception to the type system.
Stack versus heap
This still might feel too abstract to be taking such a strong stance on, so let’s consider another example of the same logic, stack vs heap. Some languages that allow you to create objects on the stack or as local members. In C++ for instance, these are very different:
1
2
3
4
struct Thing {
int* foo;
int bar;
};
Thing.foo
can be nullptr
, but Thing.bar
can’t. This is why they have different types int*
and int
respectively. Remember too, null is not zero. That’s an implementation detail. To consider that here is a category error. Once a nullptr
“becomes” a zero we’ve left the world of type abstractions so it’s meaningless to include the final representation as a part of any argument that null
isn’t an exception to the type system.
This exception to type system problem is really obvious in C++ in a way it isn’t in other languages. In languages like Java or C#, you’re always using pointers (the foo
case). What’s relevant to recognize is that the same distinction exists there and by extension to pointers in C++ as well! A pointer that maybe points to a thing is different than a pointer that definitely points to a thing.
To really drive this point home, remember that in C++ references aren’t alternative syntactic sugar for pointers, they’re different concepts. In C++, a reference is guaranteed to be non-null!
Bad in practice
Let’s say you find this unconvincing on philosophical or math grounds, after all I have no formal background in either field. All we need to do to see why this is bad is to look at how it’s used in practice. All of you have had the misfortune of encountering this code pattern before:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
internal int GetRecord(String key)
{
LogAccess(key);
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
return state[key];
}
internal void InsertRecord(String key, int value)
{
if (key is null)
{
Console.WriteLine("Cannot insert null key, no-oping");
return;
}
LogAccess(key);
state[key] = value;
}
private void LogAccess(String key)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
Console.WriteLine("Touched key: " + key);
}
Do you smell it? The secondhand smoke? This is the problem with you using nulls. Even if you want to in your code, because you’ve made your code “tolerant” of null state, my downstream or upstream code is poisoned. You might pass a null further down into my function and blow up the place, violating my invariants. Moreover, how exactly is this fixing the issue? We’ve just changed the type of exception being thrown. Either way there’s an exception, presumably with a call stack too.
This snippet may not resonate with you because of how simple it is, but try walking down some of your call stacks. Notice how 7 layers ago, you already confirmed that the value isn’t null. Worse yet, notice how every layer redundantly checks except layers 3 and 6! Now some of these other checks are dead code and despite the checks existing in some layers because the null continues to be passed down it will still throw an exception anyway!
This isn’t sound defensive programming. This is stupid. This is wearing a helmet in your kitchen because you’ve replaced the floor with ice. We can know the inputs here are fine. For instance, in InsertRecord
the function LogAccess
is only called if there’s a valid key passed. The only reason to have the null check is if we don’t trust our ability to maintain this consistently throughout the class.
But what about with internal
functions? Personally, I argue if you have that little control or consistency in your module that’s a bigger problem, so you shouldn’t have spurious null checks “just in case” their either. For public functions, they’re a necessary evil so long as it’s possible to happen.
And therein lies the real issue, why are we allowing this? Why is the type system lying to us?
The right way to do things
Put simply, don’t lie with your types. If something is A or B, then it’s not A. Even if B is “nothing”, even if B is a magic keyword. There are strategies for coping with null in legacy code & poorly designed programming languages.
Use a better type
Newer editions of languages sometimes support a “nullable type” concept. This is a flag you can opt into where the implicit nature of a null being possible becomes explicit in the typing. Examples include C# and Swift. Others encode the concept as an “optional” wrapper, e.g. std::optional<T>
in C++ and Option<T>
in Rust.
These offer differing levels of utility, but in general you should use whatever the strictest supported representation is. For instance, Rust fully eliminates null. C++’s optional doesn’t. Nothing stops you from wrapping a null pointer in the optional, but you have to mess up pretty badly. You can assume that the pointer is valid or outright enforce it with better wrapper types (such as non-null smart pointers) or static analysis tools.
In reference based languages, usually the syntax becomes Foo
for not-null types and Foo?
for maybe-null types. It’s very easy to upgrade these because before you enabled the setting, everything was already implicitly a maybe-null. In practice, not all the instance are but it’s perfectly acceptable to set them all as maybe-null in one bulk operation. You can clean these up incrementally over time, what’s important is to stem the bleeding and prohibit more debt from being added.
As you work through any language that is strictly enforcing this concept, you’ll recognize how powerful it is. Not only can we have clarity, avoid programming by coincidence, and avoid redundant “safety” checks, we can require that nulls are safely handled wherever they are possible!
A nullptr
deference induced seg fault or a NullReferenceException
are loud failures of your type system. These errors should never occur, because the tooling should enforce that you’re handling this potential scenario.
For more math oriented readers, yes I’m aware that postulates are arbitrary and you can select whichever ones you want. However, that’s not what’s going on here. People aren’t defending an alternative system. As we shall see they’re not applying it consistently.
Additionally, whether or not that exact example compiles in a particular language doesn’t change the argument. The point here is that’s a valid reading of the system, even if the particular error presents differently (say by no type could be inferred on thebar
initialization or the compiler rejecting the firstfoo = bar
on the basis that the static “types” are different even though the underlying value is equivalent and valid). ↩︎