I don't like null checking
Imagine you were using a JavaScript library, let’s call it AwesomeLibrary.
AwesomeLibrary provides you an API that lets you get the most popular Book
in a bookstore.
In other words, it provides this function:
/**
* Given a bookstore's name, return the most popular `Book`
* @param {string} bookstore Name of bookstore
* @returns {Book} The most popular book in the bookstore
*/
const getMostPopularBook = bookstore => {
let books = database.getBooks(bookstore);
let booksByPopularity = Book.sortByPopularity(books);
return booksByPopularity[0];
}
This is relatively typical code. It receives a string which represents the name of the bookstore, and returns a single object of the Book
class.
Here’s what the Book
class looks like (truncated for brevity):
class Book {
constructor(title, author, year, popularity) { ... }
getMetadata() { ... }
purchase(budget) { ... }
static sortByPopularity(books) { ... }
}
So, this allows you to write something like this:
import { getMostPopularBook } from 'awesome-library';
let bookStores = ['bookstore1', 'bookstore2', 'another bookstore'];
// The most popular books in each store
let popularBooks = bookStores.map(getMostPopularBook);
// The metadata for each popular book including title, author, year, etc.
let popularBookMetadata = popularBooks.map(book => book.getMetadata());
Now, fastidious readers may have noticed that the library doesn’t handle some edge cases.
Let’s pretend for some reason one of the stores does not have any books.
So the author of the library makes a patch, and in some stroke of madness decides to change the above function to this:
/**
* Given a bookstore's name, return the most popular `Book`
* @param {string} bookstore Name of bookstore
* @returns {Book|Carburetor} The most popular book in the bookstore, or a Carburetor
*/
const getMostPopularBook = bookstore => {
let books = database.getBooks(bookstore);
// Look here:
if (books.length === 0) return new Carburetor();
let booksByPopularity = Book.sortByPopularity(books);
return booksByPopularity[0];
}
The function can now return either a Book
or a Carburetor
.
Although the function indicates you should expect a Book
, it can sometimes return an object of a totally unrelated class called Carburetor
.
Does this seem ridiculous to you?
I hope it does!
So, why wouldn’t the below piece of code seem equally ridiculous?
/**
* Given a bookstore's name, return the most popular `Book`
* @param {string} bookstore Name of bookstore
* @returns {Book|null} The most popular book in the bookstore, or null
*/
const getMostPopularBook = bookstore => {
let books = database.getBooks(bookstore);
// Look here:
if (books.length === 0) return null;
let booksByPopularity = Book.sortByPopularity(books);
return booksByPopularity[0];
}
Remember, null
is an object!
If it is absurd to return an object of class Carburetor
from a function called getMostPopularBook
, why is it acceptable to return null
?
By the way, null
is an object, just like an instance of Carburetor
.
You can verify by this in your browser’s dev console by typing typeof null
. You’ll see "object"
.
Null is an object!
NULL.
IS.
AN.
OBJECT.
What this does to my code
Let’s face it, we’ve all written code like that. We’ve all also used code like that.
I don’t like it at all, because what it does is it forces me, a user of the library to do things like this:
import { getMostPopularBook } from 'awesome-library';
let bookStores = ['bookstore1', 'bookstore2', 'another bookstore'];
let popularBooks = bookStores.map(getMostPopularBook);
// See the extra filter
let popularBookMetadata = popularBooks.filter(book => book !== null).map(book => book.getMetadata());
Sometimes, that also comes in this form, which is even worse (slightly exaggerated):
import { getMostPopularBook } from 'awesome-library';
let bookStores = ['bookstore1', 'bookstore2', 'another bookstore'];
let popularBooks = bookStores.map(getMostPopularBook);
let budget = new Budget(200.00);
for (let book in popularBooks) {
// null check here
if (book) {
let metadata = book.getMetadata(); // can return null
// another null check because `Book` throws an exception if
// the remaining budget is insufficient
if (metadata && metadata.price <= budget.remaining) book.purchase(budget);
}
else console.log('we ran out of money');
}
It’s just… ugly.
I have to guard against null
at all times, because I can’t rely on things like getMostPopularBook
, to… you know, return a Book
.
I don’t like writing code that way.
What I’d prefer
Where possible, I’d prefer an object with the same interface is returned, instead of null
.
The library could return, for example, a NullBook
:
class NullBook extends Book {
getMetadata() { return { price: 0 } }
purchase(budget) { /* does nothing */ }
}
Then I could call all the methods without having to do all these null
checks.
Unfortunately, I don’t think this is possible all the time.
And since it’s rare to see this in JavaScript (or Python for that matter), it can also be argued that this isn’t “idiomatic”.
Sigh. I’ll just go back to null
checking.
What about undefined
?
@#$#%%($%)!!!!
Some resources
Lots of people have talked about this before, with some proposed solutions (like the one I described). Check out these resources: