When researching ways of writing cleaner and more robust code, there’s a multitude of materials that debate the existence of this concept in an object-oriented language. This can be surprising, since in practice we see “null” everywhere, yet these blogs, articles, and videos make it seem like it’s the enemy of beautiful, resilient code.
So, what are the reasons why “null” might be considered “bad?” What, if anything, can we do about it?
The quick conclusion
For the impatient among us, the quick conclusion is that completely eliminating “null” is pretty much impossible for a language that already supports it. In such cases, even avoiding “null” might be more effort than it’s worth, especially in a language where this concept already permeates everywhere, including its base libraries. However, reducing its usage could allow programmers to develop some good, worthwhile practices.
There are several reasons why “null” is said to be a “bad” concept:
1. Ambiguous at best
A “null” value tries to represent several concepts (“nothing,” “undefined,” “not valid,” etc.) under the same umbrella. The way to interpret such a value varies greatly, not just from person to person, but also depending on the situation or context in which it is being used.
While at face value this doesn’t seem to be a huge problem, the more insidious issue is that it hides intent from the caller. This forces the caller to look into the implementation details, debug, and check for the real meaning behind that value. If the caller fails to take action, then the only alternative is to assume the meaning and possibly miss the correct way to handle it.
For business related classes a value of “null” often doesn’t really make sense. It’s an ambiguous technical term related to implementation, while a business class should reflect an object in reality. For instance, if a method searches for an employee and doesn’t find anything, returning “null.” What does a “null” employee even mean? Of course a caller might be confused when receiving such a value, forcing the focus to be switched from handling an employee business object to a technical “null” value.
2. Guard clauses
Once a method is capable of returning “null” in any of its branches, it can lead to a lot of bloat in the code. This is also a bit irresponsible because returning “null” passes the handling of an internally invalid or unexpected state from the method to its callers, forcing the callers to deal with details they should not even know about. Now all callers are forced to check for, handle, and possibly even propagate “null” to its own callers. And if a caller fails to do so, then the runtime–and the clients–will surely notice the crash caused by the missing check.
Fortunately, recent versions of the .NET framework compilers can help identify where a guard clause might be needed by statically checking for possible “null” usages.
3. A special case that is a bit too special
Although there are plenty of examples of special values, using “null” requires a lot more consideration and mental modeling when using it.
An empty string or the PI constant are special values, but the difference is that any method that you can call on a random string, you can also call on an empty string as well. Any method you can call on a number you can also call on the PI constant. “Null,” however, is quite the opposite; it tries to represent a lack of value while being assigned just like a normal value. For example, you can do anything with an employee object unless it’s “null,” “ToLower” can be used on any string unless it’s “null,” and it is highly likely you will only get this feedback at runtime when it can be too late to return to normal operations.
Also, there’s plenty of inconsistencies throughout frameworks and languages on how to handle operations that involve “null” values. The .NET framework has always been pretty consistent on that front fortunately.
Some proposed solutions
1. Optional types
Optional types have been suggested as an alternative to “null” by wrapping a value into an object that also exposes, for example, a Boolean indicating if a value is present or not. Although this does solve the problem of the polymorphic nature of “null,” since optional types are often provided as a templated class, in practice it does little to solve the aforementioned issues because:
a) Optional types just replace checks for “null” with checks on the Boolean property of the optional type
b) Using optional types suffers from the same issue of proliferation along the call chain as “null”
c) Just replacing a generic “null” with another generic optional type is often not enough. For example, when a search for a user yields no results one might want to return a generic anonymous user with part of the functionality of a normal user.
2. Throwing exceptions
Instead of propagating “null” or optional types we could throw exceptions in case a rule or result is not what we expected. At first glance this looks like a good approach, and for cases where the invalid value is truly unexpected an exception is a good practice. However, careful consideration must be taken into what really is an unexpected event.
Exceptions are often misused and overused as crude ways to change the control flow of a business operation. There are not that many errors that are completely unrecoverable and must result in a full stop of an operation. Remember that not all errors are exceptions.
For example, let’s say that we save some configuration for a user in a file and we have a method that reads the configuration from that file on user login. From the business’ point of view, does it make sense to prevent a user login because the configuration file could not be opened? Site owners would not want to prevent users from using their shop for that reason and would much rather use a default set of configurations.
Knowing the business domain and talking to business experts will convert a lot of those exceptions into alternative error handling procedures. Still, this seems to be a common and easy trap to fall into, and some languages have dropped exception handling altogether (for example, GO).
3. Avoiding “null” as much as possible
We’ve already covered that avoiding “null” completely is not possible, since the .NET framework itself and its base class libraries make no effort in specifically avoiding “null.” That being said, what would it take to significantly reduce the usage of “null,” especially in the core business implementation, which is the part of software where changes occur more often? Well, it takes a lot of attention and communication combined with implementing some basic patterns to isolate the business core as much as possible. Some examples of steps that can be taken are:
a) Since we have no control over third party libraries, we could (and likely should), hide them behind something. The adapter pattern comes to mind and this is, in general, a good approach whether we try to avoid “null” or not.
Another good approach would be to implement an architectural pattern. For instance, the “Onion” architectural pattern is good at isolating business code from the rest of the system. Remember that database, UI, and file systems are all like external libraries that usually have little or nothing to do with the core business.
Image inspiration: https://dzone.com/articles/onion-architecture-is-interesting
b) Dependency injection has seen a lot of adoption, in part encouraged by the framework itself as, for example, ASP.NET Core already comes with a built-in dependency injection container. Therefore, since an unregistered dependency just replaces the “null” reference exception with a different exception type, we can try to validate the registered dependencies. The built in dependency container from ASP.NET Core can validate its dependencies at startup, although with some limitations. One limitation of this approach is that dependencies directly injected in controllers are not validated because of the special way they are registered in the default ASP.NET dependency container.
c) Carefully watch for any call to the base class library for methods that might return null (for example, FirstOrDefault in a LINQ expression). Unfortunately this is harder to do by hiding it behind an adapter since it’s such a common and basic functionality (it’s part of the base class library, after all).
Unfortunately, for .NET avoiding “null” is impossible. But in all other cases, trying to avoid “null” as much as possible can significantly reduce code clutter and bring what actually matters to the forefront– without getting lost in lots of easily avoidable statements. It also encourages business code isolation from the rest of the system, while helping programmers adopt good patterns and practices. So while avoiding “null” is challenging, it’s worth it.