“The road to hell is paved with good intentions.”
This ancient proverb is just as true in software development as it is in life. Take the following code as an example:
public List<Product> getProducts(String country) { if (country == null) { country = „at“; } List<Product> products = database.getProductsForCountries(country); if (products == null) { return Collection.emptyList(); } else { return products; } }
Don’t sweep it under the carpet
The function in our example returns a list of products for a given country. If the value of the country is null, then we simply set a default value (Austria in our case) and continue like everything is fine.
We also consider the possibility that the underlying database function can return null. Again, we overwrite the original result and return an empty list rather than null.
What is the problem here? These steps make our application as error-proof as possible. That is a good attitude to take with our code.
In a case like this, however, our good intentions lead to unfortunate results. This pattern hides potential bugs. Even worse, the bugs become hard to discover.
When users send us bug reports
Imagine the calling function resolves the current user’s country with GeoIP. We would never know if it ever malfunctions and returns null. The application will behave as if everything is normal. How would we know that all of our customers are now Austrian? If most of our customers are Austrian, the bug will be easier to overlook.
Unit tests won’t help us here because this is a conceptual bug caused by not thinking about the consequences of our coding actions.
Only when our customers complain that they don’t want to pay in euros since they use dollars, or when our shipping agents point out that Los Angeles is not in Austria, only then do we realize that things aren’t working the way we thought they were.
Growing code base
It should come as no surprise that multiple arguments in our functions, or dependencies to other classes, will require null checks. Our example only has one with the ‘database’ object yet it caused many problems.
Each additional null check introduces a new branch that requires its own unit test. Each branch distracts the reader from the code’s original intent.
A large number of unit tests dedicated solely to null checks increases our code base unnecessarily. Remember that we have to maintain those checks. If we modify our code, then we have to adapt all of these additional tests.
All of this additional work erodes the discipline required for effective TDD.
Treat an exception for what it is: An Exception
Null values break the execution instantly. That is a nice behaviour. Let it happen. We want to see a big NullPointerException error message popping up on our dev machine when something is wrong with the system’s state.
Don’t continue like nothing has happened.
What if a NullPointerException is never thrown? Well then, it means our code never access the object and we probably don’t require it at all. I would not file that issue as a bug but as potential dead code.
But it can be null!
It’s all too easy to think that null values are automatically wrong. Please keep in mind that null values can be perfectly valid results. For example, users can request baskets that don’t exist yet. We can use Java’s Optional in such a case - or declare an Exception or whatever your language of choice has to offer you.
Never expect that the caller or producer corrects your “mistakes”. Show them that returning a null is a valid scenario that one must take care of.
We should remove all validations? Are you serious?
No, not at all! It is more the opposite. We should make validity checks like complexity criteria, a new registration with an existing email and so on whenever possible.
My whole point here is that you should not overwrite default values for nulls. We don’t just want validation. We want exceptions thrown too often rather than too rarely.
In the case of a null, we don’t need to bother with manual validation. The runtime does that for us.
Different game for API or library vendors
Things are different when we provide an API or a library. In short, our end user is a technical person or a machine.
In that case, we want to return a detailed error description. The end user needs to learn exactly what is wrong in the request message in order to fix it.
Wrapping up
See what happens when we reduce our initial code example to the following:
public List<Product> getProducts(String country) { return database.getProductsForCountries(country); }
If the country is null, then your database library will throw an error message. If the database returns a null, then your caller function will throw a NullPointerException. The application breaks either way.
Let’s not overuse validity checks. This will keep our code base very short and clear.
Our runtime should throw a NullPointerException
- or an UncaughtTypeError
or whatever it is called in our language. Never let a StackTrace break through. Show the end user a nice, generic error message.
At the same time, make sure you have a central error-logging application, such as Sentry, in place to collect all available information like StackTrace, context information, executed SQL statements, and so forth.
As counter-intuitive as it sounds, letting our runtime throw null errors lets us as developers fix the root cause quickly and safely.
My final advice: don’t underestimate the productivity gain you get from code deletion.
Application shows user error page. In the logs you see NPE. The proper way here is to understand where the null value came from (the root cause) and fix it or (if that’s not possible) adding verbose log message about the context and/or rethrowing the exception (again with more details) to catch it elsewhere in order to provide nice communication to the user. If I only got a dollar every time I saw a support developer fixing such case by if-null clause wrapping the problematic code I would be filthy rich.
Exactly, I also want to highlight the importance of a sophisticated log collector. It should not only be able to collect logs, but should also provide you with an UI and various tools to support you with the analysis of the bugs.