The Null Problem
-
•7 min read
If you write C# for long enough, you will eventually come across a NullReferenceException. It's one of the most common errors there is and it's usually the first one people learn to recognise. I got curious about why it's so common which led me to a surprising story about where null came from.
The billion-dollar mistake #
The null reference was invented in 1965 by Tony Hoare while he was designing a programming language called ALGOL W. His reason for adding it was that it was so easy to implement. Decades later in a talk in 2009, he called it his "billion-dollar mistake". He said it probably caused countless errors, vulnerabilities and system crashes over the years.
The idea may sound harmless since Null is a special value that means "there's nothing here". The problem is that it can be put in the place of any object so any variable could secretly hold nothing at all. Traditional compilers won't be able to tell you until it's too late.
Person person = FindPerson("nobody"); // returns null if no one is found
Console.WriteLine(person.Name); // NullReferenceException
Nothing looks wrong with that code. It compiles and it only fails when it runs and only when FindPerson doesn't find anyone. This means it can work in testing for weeks and then crash in production because of one unexpected value.
Defending against it #
The usual fix is to check for null before using something:
if (person != null)
{
Console.WriteLine(person.Name);
}
This is defensive programming and it's a good idea to do it. But it can be tedious to check for null everywhere. This also means the checks can pile up fast. Soon they can be everywhere across a codebase. Some places check and some places assume the value is never null so it's hard to tell which is which. There's also the question of what null even means. Does a method return null because it found nothing, because something went wrong or because no one has set a value yet? Null can mean all of those things at once.
What modern C# does about it #
C# has been adding tools to make this less painful. The ?. operator stops the call if the value is null, and ?? lets you give a fallback value.
Console.WriteLine(person?.Name ?? "Unknown");
The bigger change is nullable reference types, which arrived in C# 8. When it's turned on (it's on by default in new projects since .NET 6), a normal Person is no longer allowed to be null and a Person? is a value that might be. The compiler then keeps track of it for you. With the example above, using Person? makes the compiler warn about the line that would have crashed:
Person? person = FindPerson("nobody");
Console.WriteLine(person.Name); // warning CS8602: Dereference of a possibly null reference
This moves the problem from runtime to build time which is much better. The one thing to watch out for is that these are warnings and not errors so the code still builds and still crashes if the warning is ignored. You can turn warnings into errors if you want to be strict.
Takeaways #
- Treat null as a decision and not a default. If something can be missing, make it clear in the type with
?. - Return an empty list instead of null when a method has no results.
- Turn on nullable reference types and take the warnings seriously.
Null probably isn't going away but it's definitely much easier to deal with now that the compiler can help. Other languages went further. For example, Rust has no null at all and uses an Option type to say a value might be missing so you have to handle both cases before the code will compile.