[object Object]
First published at Tuesday 3 May 2016
Warning: This blog post is more then 8 years old – read and use with care.
Never Use null
When doing code reviews together with our customers we see a pattern regularly which I consider problematic in multiple regards – the usage of null
as a valid property or return value. We can do better than this.
Let's go into common use cases first and then discuss how we can improve the code to make it more resilient against errors and make it simpler to use. Most issues highlighted are especially problematic when others are using your source code. As long as you are the only user (which hopefully is not the case) those patterns might be fine.
What it is Used For
One common use case is setter injection for optional dependencies, like:
class SomeLoggingService {
private $logger = null;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
}
The logger will be set in most cases but somebody will forget this when using your service. Now a second developer enters the scene and writes a new method in this class using the property $logger
. The property is always set in the use cases they test during development, thus they forget to check for null
– obviously this will be problematic in other circumstances. You rely on methods called in a certain order which is really hard to document. An internal getLogger()
method constructing a default null logger might solve this problem, but it still might not be used because the second developer wasn't aware of this method and just used the property.
In PHP versions < 7 a call to $this->logger->notice(…)
will result in a Fatal Error
which is particularly bad since the application can't handle this kind of errors in a sane way. In PHP 7 those errors are finally catchable but still nothing you'd expect in this situation.
What is even worse is debugging this kind of initialization. This is often even used together with aggregated objects which are required by the aggregating class. (You should not use setter injection for mandatory aggregates, but it is still used this way.) Let's consider the following code now:
class SomeService {
public function someMethod() {
$this->mandatoryAggregate->someOtherMethod(/* … */);
}
}
When calling someMethod()
and the property $mandatoryAggregate
is not initialized we get a fatal error, as mentioned. Even if we get a backtrace through XDebug or change the code to throw an exception and get a backtrace it is still really hard to understand why this property is not initialized since the construction of SomeService
usually happens outside of the current callstack but inside the Dependency Injection Container or during application initialization.
The debugging developer is now left with finding all occurrences where SomeService
is constructed, check if the $mandatoryAggregate
is properly initialized and fix it, if not.
The solution
All mandatory aggregates must always be initialized during construction. If you want a slim constructor consider a pattern like the following:
class SomeService {
public function __construct(Aggregate $aggregate, Logger $logger = null) {
$this->aggregate = $aggregate;
$this->logger = $logger ?: new Logger\NullLogger();
}
}
The parameter $aggregate
now is really mandatory, while the logger is optional – but it will still always be initialized. The Logger\NullLogger
now can be logger which just throws all log messages away. This way there is no need to care about checking the logger every time you want to use it.
Use a so called null object if you need a default instance which does nothing. Other examples for this could be a null-mailer (not sending mails) or a null-cache (does not cache). Those null objects are usually really trivial to implement. Even it costs time to implement those you'll safe a lot time in the long run because you will not run in Fatal Errors
and have to debug them.
null
as Return Value
A similar situation is the usage of null
as a return value for methods which are documented to return something else. It is still commonly used in error conditions instead of throwing an exception.
It is, again, a lot harder to debug if this occurs in a software you use but you are not entirely familiar with. The null
return might pass through multiple call layers until it reaches your code which makes debugging that kind of code a journey through layers of foreign and undiscovered code – sometimes this can be fun but almost never what you want to do when in a hurry:
class BrokenInnerClass {
public function innerMethod() {
// …
if ($error) {
return null;
}
// …
}
}
class DispatchingClass {
public function dispatchingMethod() {
return $this->brokenInnerClass->innerMethod();
}
}
class MyUsingClass {
public function myBeautifulMethod() {
$value = $this->dispatchingClass->dispatchingMethod();
$value->getSomeProperty(); // Fatal Error
}
}
Usually there are even more levels of indirection, of course. We live in the age of frameworks after all.
The solution
If a value could not be found do not return null
but throw an exception – there are even built in exceptions for such cases like the OutOfBoundsException
, for example.
In the callstack I can see immediately where something fails. In the optimal case the exception message even adds meaning and gives some hints of what I have to fix.
Summary
Using null
can be valid inside of value objects and sometimes you just want to show nothing is there. In most cases null
should be either replaced by throwing an exception or providing a null object which fulfills the API but does nothing. Those null objects are trivial and fast to develop. The return on investment will be huge due to saved debugging hours.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: