Why you should avoid using null
March 30, 2019
Java, python, JavaScript, C/C++. These each support null
. In fact, most
languages you learn in school or use in your day job there is way to represent
null
- an empty value. Essentially programmers needed a way to store an
optional or empty objects. (for a really good history and talk about null pointers and
where they came from checkout this talk by Tony Hoare
For instance and object representing a person might look like this:
const person = {
firstName: 'John',
middleName: 'Jacob',
lastName: 'Smith',
title: 'Mr.'
};
However, for child, the title
field seems inappropriate, so it’s given the
value of null
:
const child = {
firstName: 'Jimmy',
middleName: 'John',
lastName: 'Smith',
title: null
};
This seems fine until you need to do a little processing on the data:
function getGreeting(person) {
return `Dear ${person.title} ${person.firstName} ${person.lastName}`;
}
For John, getGreeting
will output Dear Mr. John Smith
, but for Jimmy we’ll
get Dear null Jimmy Smith
. Not something you’ll want to publish on a user
account page or send in a newsletter. The most obvious solution is just to
check for null:
function getGreeting(person) {
return `Dear ${person.title ? `${person.title} ` : ''}`
+ `${person.firstName} ${person.lastName}`;
}
This gives us what we want, but what about the case when firstName
and lastName
are null?
function getGreeting(person) {
return `Dear ${person.title ? `${person.title} ` : ''}`
+ `${person.firstName ? `${person.firstName} ` : ''}
+ `${person.lastName ? `${person.lastName}` : ''}`;
}
And now what about the case when the actual person object is null?
function getGreeting(person) {
if (person === null) {
// Should I throw an error or return an empty string?
}
return ...
}
Quickly the code gets out of hand and what was meant to be a simple function to produce a human consumable string turned into a series of if else and ternary statements wreaking havoc on readability. Suddenly a peer review becomes reading null checks and execution clutters up your CPU cycles with branching statements.
Simplify your code
Assuming that there will be no nulls in the person object and that the person
object is not null, the getGreeting
function can be rewritten:
function getGreeting(person) {
const greeting =
`Dear ${person.title} ${person.firstName} ${person.lastName};
return person.replace(/\s+/g, ' ');
}
The function getGreeting
simplifies down to two lines. The first builds a
greeting and since null is not a valid value for any of our fields we can
assume they are strings and therefore we can just concatenate them together.
The second line handles empty strings by replacing all occurrences of one or
more space characters with a single space. The function’s intent is clear.
Responsibility of safe type checking moves further up the chain relieving
getGreeting
of unnecessary if-else clauses.
The verifying of correct values still needs to be done somewhere in our
software stack since that responsibility shifted away from getGreeting
, but
hasn’t landed anywhere else.
If getGreeting
belongs to a library intended to be imported by other
developers, then clear documentation would suffice. This shifts
responsibility to the outside developer to call your code correctly and
implement their own safety checks before using getGreeting
. This isn’t bad
approach, albeit a lazy one.
Checking parameters on initialization is a bit more elegant:
function checkIsString(value, name) {
if (typeof value !== 'string') {
throw new TypeError(
`${name} should be a string, got ${value} instead`
);
}
}
function createPerson(attributes) {
let { firstName, lastName, middleName, title } = attributes;
checkIsString(firstName, 'firstName');
checkIsString(lastName, 'lastName');
checkIsString(middleName, 'middleName');
checkIsString(title, 'title');
return {
getGreeting() {
const greeting =
`Dear ${title} ${firstName} ${lastName};
return person.replace(/\s+/g, ' ');
}
};
}
Upon invocation, createPerson
expects an object with four fields. Rather
than checking for null or undefined each field is checked to be a string. If
anyone is not, then an Error will be thrown that states which field was not a
string.
Now, getGreeting
is a function of an object with private members. Instead of
checking types itself, the function relies on the verification provided in the
constructor. This creates safety with clear error messages and reduces
branching statements in the code.
JavaScript offers no intrinsic support for type checking therefore the four
calls to checkIsString
were added. However, Typescript offers an even
cleaner way to remove null checks with a statically-checked type
system.
interface Person {
firstName: string,
lastName: string,
middleName: string
title: string
}
function greetPerson(p: Person) {
const greeting = `Dear ${tile} ${firstName} ${p.lastName}`;
return greeting.replace(/\s+/g, ' ');
}
As long as the strictNullChecks
Typescript flag is set, this code:
let p: Person = {
firstName: 'John',
lastName: 'Smith',
middleName: 'Jacob',
title: null
};
…results in the following error:
error TS2322: Type 'null' is not assignable to type 'string'.
Typescript does static analysis, meaning it looks at code instead of running it to check for Errors. This means these checks are done before node or a user’s browser ever runs them. The final application doesn’t take a performance hit.
Caveat
Typescript comes with a type any
which can be anything including null
and
undefined
. Any other type in the system will allow a variable typed as any
to be assigned to it. Consequently, the following code is valid:
let x: any = null;
let p: Person = x;
This presents a problem if we are populating our Person object with data from
an outside source such as a database. If our database allows NULL
entries,
then any nulls from the database need conversion to empty strings. If possible,
the database schema should be updated to have NOT NULL
constraints. When
updating the schema isn’t feasible, make sure your code that reads the database
is typed to disallow null
.
For and example:
class Person {
private firstName: string = '';
private middleName: string = '';
private lastName: string = '';
private title: string = '';
constructor(person) {
Object.keys(person).forEach((property: string) => {
if (typeof this[property] === 'string') {
this[property] = typeof person[property] === 'string'
? person[property]
: '';
});
}
}
db.each('SELECT firstName, lastName, middleName, title FROM person', (error, person: Person) => {
console.log(greetPerson(new Person(person)));
});
Again, using variable sanitation in a constructor is used. However, rather than
throwing a TypeError
an empty string replaces invalid input.
Reducing errors
Ultimately removing null
is a mechanism for reducing errors. By working
within constraints encourages creativity and helps reduce code complexity. It
enables type checking at compile time and reduces branching statements. While
avoiding null
and checks for null
may not be feasible for every project,
it’s worth trying out next time you open your editor.
I am Ryan Seal and have been developing software professionaly for the last 12 years. Currently I live and work in South Africa wirting software for a non-profit organization.