Let’s say you are working in the kitchen and preparing a five-course meal. A five-course meal is a considerable amount of work, but fortunately, you have an assistant who can help you with the mise en place. You’ve asked your assistant to cube some Russet potatoes, julienne some baby carrots, and dice some red onions. After 15 minutes of working, you check on your assistant’s progress and ...
What a disaster! You have diced onions to cook with, but you’ve also got a mess to clean up.
This is how I feel when I use a method with the following inside:
return null
I’m seeing thousands of return null
statements in recent code reviews. Yes, I know you’ll find my fingerprints on some of that code, but as my mom used to say: "live and learn".
An ideal function, like an assistant cook, will encapsulate work and produce something useful. A function that returns a null reference achieves neither goal. Returning null
is like throwing a time bomb into the software. Other code must a guard against null
with if
and else
statements. These extra statements add more complexity to the software. The alternative is to allow null
to crash the program with exceptions. Both scenarios are bad.
Before throwing in a return null
statement, or using LINQ operators like FirstOrDefault
, consider some of these other options.
In some scenarios the right thing to do is to throw an exception. Throwing is the right approach when null
represents an unrecoverable failure.
For example, imagine calling a helper method that gives you a database connection string. Imagine the method doesn’t find the connection string, so the method gives up and returns null
. Your software doesn’t function without a database, so other developers write code that assumes a connection string is always in place. The code will try to establish connections using null connection strings. Eventually, some component is going to fail with a null reference exception or null argument exception and give us a stack trace that doesn’t point to the real problem.
Instead of returning null
, the method should throw an exception with an explicit message like "Could not find a connection string named 'PatientDb'". Not only does the software fail fast, but the error message is easier to diagnose compared to the common null reference exception.
Throwing is not always the right option, however. Consider the case where a user is searching for a specific patient using an identifier. A method to execute the search has to consider the possibility that a user is searching with a bad or obsolete identifier, and not every search will find a patient. In this scenario it might be better to return null
than throw an exception. But, there are other options, too.
Finding a sensible and safe default value works in some scenarios. For example, imagine a helper method to fetch the claims for a given user. What should the method do when no claims are found? Instead of returning null
and forcing every caller to guard against a null collection of claims, consider returning an empty collection of claims.
private static readonly IReadOnlyDictionary<string, string> EmptyClaims = new Dictionary<string, string>(); public IReadOnlyDictionary<string, string> FindClaims(int userId) { /* * Find claims from somewhere, or ... */ // when no claims are found . . . return EmptyClaims; }
If callers only use ContainsKey
or TryGetValue
to make authorization decisions, the empty claims collection works well. There are no claims in the empty collection, so the software will never authorize the user. Even better, the code doesn’t need to guard against a null reference.
Think of all the complexity you've removed from the rest of the software! Future generations who work on the code will praise you and build ornate shrines in your honor.
The null object pattern has been around for a long time because the pattern is successful at reducing complexity and making software easier to read and write. Imagine a method that needs to return the current user in the system, but the method can’t identify the user. Perhaps the method can’t identify the user because the user is anonymous, and not being able to find the user is an expected condition. We shouldn’t handle an expected condition using exceptions.
Another option is to use null
to represent an unknown user, but returning null is lazy. In case you missed the opening paragraphs, returning null is like telling your callers "I’m not only giving up, but I’m going to stick you with a dangerous value, so handle with care, and good luck!". Future generations who work on the code will curse you and burn you in effigy.
The null object pattern solves the problem by returning a real object reference, and the real object contains some safe defaults. We don’t want an unidentified user to gain access to admin functionality, or somehow slip through permission checks, so we create a version of the user that will fail all permission checks and not display a name, but at the same time no code needs to guard against null and introduce if else checks everywhere.
There are a number of different approaches you can use to design null objects, but remember the goal is to build an object with safe defaults. Here’s an example. Let’s start with a User
type.
public class User { public User(string name, IReadOnlyDictionary<string, string> claims) { Name = name; Claims = claims; } public string Name { get; protected set; } public IReadOnlyDictionary<string, string> Claims { get; protected set; } public bool IsAdmin { get; } }
Now we can create an instance of User
that contains safe defaults, like an empty claims dictionary so the IsAdmin
helper (which presumably looks into Claims, will return false). One way to define the class is to use inheritance, like so:
public class NullUser : User { public NullUser() : base(string.Empty, EmptyClaims) { } private static readonly IReadOnlyDictionary<string, string> EmptyClaims = new Dictionary<string, string>(); }
Now we can write a method that always return a safe value.
private static readonly NullUser nullUser = new NullUser(); User GetCurrentUser() { // if user not found : return nullUser; }
If we start thinking about alternatives to null
today, we'll be better set to handle the future.
There is a new feature coming with version 8 of the C# compiler that will help us avoid nulls and all the problems they bring. The feature is non-nullable reference types. I expect most teams to aggressively adopt C# 8 after the release later this year, at least for new code. You can find many examples of the new feature in Microsoft docs and blogs, but the idea is to tell the C# compiler if a null value is allowed, or not, in each variable. The C# compiler can aggressively check code to make sure non-nullable reference types do not receive a null value. Using these types should give us simpler code, and force us to think before adding a return null
.
Ten years ago, Tony Hoare apologized for inventing null
, calling the null reference a billion dollar mistake. Let's see if we can avoid using his mistake!