DEV Community

Cover image for Unhealthy Code: Null Checks Everywhere!
James Hickey
James Hickey

Posted on • Edited on • Originally published at blog.jamesmichaelhickey.com

Unhealthy Code: Null Checks Everywhere!


This is an excerpt from my book Refactoring TypeScript: Keeping Your Code Healthy.

Refactoring TypeScript book


Identifying The Problem

Billion Dollar Mistake

Did you know that the inventor of the concept of "null" has called this his "Billion Dollar Mistake!"

As simple as it seems, once you get into larger projects and codebases you'll inevitably find some code that goes "off the deep end" in its use of nulls.

Sometimes, we desire to make a property of an object optional:



class Product{
  public id: number;
  public title: string;
  public description: string;
}


Enter fullscreen mode Exit fullscreen mode

In TypeScript, a string property can be assigned the value null.

But... so can a number property!



const chocolate: Product = new Product();
chocolate.id = null;
chocolate.description = null;


Enter fullscreen mode Exit fullscreen mode

Hmmm....

Another Example

That doesn't look so bad at first glance.

But, it can lead to the possibility of doing something like this:



const chocolate: Product = new Product(null, null, null);


Enter fullscreen mode Exit fullscreen mode

What's wrong with that? Well, it allows your code (in this case, the Product class) to get into an inconsistent state.

Does it ever make sense to have a Product in your system that has no id? Probably not.

Ideally, as soon as you create your Product it should have an id.

So... what happens in other places that have to deal with logic around dealing with Products?

Here's the sad truth:



let title: string;

if(product != null) {
    if(product.id != null) {
        if(product.title != null) {
            title = product.title;
        } else {
            title = "N/A";
        }
    } else {
        title = "N/A"
    }
} else {
    title = "N/A"
}


Enter fullscreen mode Exit fullscreen mode

Is that even real code someone would write?

Yes.

Let's look at why this code is unhealthy and considered a "code smell" before we look at some techniques to fix it.

Is It That Bad?

This code is hard to read and understand. Therefore, it's very prone to bugs when changed.

I think we can agree that having code like this scattered in your app is not ideal. Especially when this kind of code is inside the important and critical parts of your application!


A Side-Note About Non-Nullable Types In TypeScript

As a relevant side note, someone might raise the fact that TypeScript supports non-nullable types.

This allows you to add a special flag to your compilation options and will prevent, by default, any variables to allow null as a value.

A few points about this argument:

  • Most of us are dealing with existing codebases that would take tons of work and time to fix these compilation errors.

  • Without testing the code well, and carefully avoiding assumptions, we could still potentially cause run-time errors by these changes.

  • This article (taken from my book) teaches you about solutions that can be applied to other languages - which may not have this option available.


Either way, it's always safer to apply smaller more targeted improvements to our code. Again, this allows us to make sure the system still behaves the same and avoids introducing a large amount of risk when making these improvements.

One Solution: Null Object Pattern

Empty Collections

Imagine you work for a company that writes software for dealing with legal cases.

As you are working on a feature, you discover some code:



const legalCases: LegalCase[] = await fetchCasesFromAPI();
for (const legalCase of legalCases) {
    if(legalCase.documents != null) {
        uploadDocuments(legalCase.documents);
    }
}


Enter fullscreen mode Exit fullscreen mode

Remember that we should be wary of null checks? What if some other part of the code forgot to check for a null array?

The Null Object Pattern can help: you create an object that represents an "empty" or null object.

Fixing It Up

Let's look at the fetchCasesFromAPI() method. We'll apply a version of this pattern that's a very common practice in JavaScript and TypeScript when dealing with arrays:



const fetchCasesFromAPI = async function() {
    const legalCases: LegalCase[] = await $http.get('legal-cases/');

    for (const legalCase of legalCases) {
        // Null Object Pattern
        legalCase.documents = legalCase.documents || [];
    }
    return legalCases;
}


Enter fullscreen mode Exit fullscreen mode

Instead of leaving empty arrays/collections as null, we are assigning it an actual empty array.

Now, no one else will need to make a null check!

But... what about the entire legal case collection itself? What if the API returns null?



const fetchCasesFromAPI = async function() {
    const legalCasesFromAPI: LegalCase[] = await $http.get('legal-cases/');
    // Null Object Pattern
    const legalCases = legalCasesFromAPI || [];

    for (const case of legalCases) {
        // Null Object Pattern
        case.documents = case.documents || [];
    }
    return legalCases;
}


Enter fullscreen mode Exit fullscreen mode

Cool!

Now we've made sure that everyone who uses this method does not need to be worried about checking for nulls.

Take 2

Other languages like C#, Java, etc. won't allow you to assign a mere empty array to a collection due to rules around strong typing (i.e. []).

In those cases, you can use something like this version of the Null Object Pattern:



class EmptyArray<T> {
    static create<T>() {
        return new Array<T>()
    }
}

// Use it like this:
const myEmptyArray: string[] = EmptyArray.create<string>();


Enter fullscreen mode Exit fullscreen mode

What About Objects?

Imagine that you are working on a video game. In it, some levels might have a boss.

When checking if the current level has a boss, you might see something like this:



if(currentLevel.boss != null) {
    currentLevel.boss.fight(player);
}


Enter fullscreen mode Exit fullscreen mode

We might find other places that do this null check:



if(currentLevel.boss != null) {
    currentLevel.completed = currentLevel.boss.isDead();
}


Enter fullscreen mode Exit fullscreen mode

If we introduce a null object, then we can remove all these null checks.

First, we need an interface to represent our Boss:



interface IBoss {
    fight(player: Player);
    isDead();
}


Enter fullscreen mode Exit fullscreen mode

Then, we can create our concrete boss class:



class Boss implements IBoss {
    fight(player: Player) {
        // Do some logic and return a bool.
    }

    isDead() {
        // Return whether boss is dead depending on how the fight went.
    }
}


Enter fullscreen mode Exit fullscreen mode

Next, we'll create an implementation of the IBoss interface that represents a "null" Boss:



class NullBoss implements IBoss {
    fight(player: Player) {
        // Player always wins.
    }
    isDead() {
        return true;
    }
}


Enter fullscreen mode Exit fullscreen mode

The NullBoss will automatically allow the player to "win", and we can remove all our null checks!

In the following code example, if the boss is an instance of NullBoss or Boss there are no extra checks to be made.



currentLevel.boss.fight(player);
currentLevel.completed = currentLevel.boss.isDead();


Enter fullscreen mode Exit fullscreen mode

Note: This section in the book contains more techniques to attack this code smell!

How To Keep Your Code Healthy

This post was an excerpt from Refactoring TypeScript which is designed as an approachable and practical tool to help developers get better at building quality software.

Refactoring TypeScript book

Keep In Touch

Don't forget to connect with me on:

Navigating Your Software Development Career Newsletter

An e-mail newsletter that will help you level-up in your career as a software developer! Ever wonder:

✔ What are the general stages of a software developer?
✔ How do I know which stage I'm at? How do I get to the next stage?
✔ What is a tech leader and how do I become one?
✔ Is there someone willing to walk with me and answer my questions?

Sound interesting? Join the community!

Oldest comments (48)

Collapse
 
ankitasinghal profile image
Ankita Singhal

This is a nice article. Cool tips. I do see a lot of code with checks for null/undefined.

I myself am guilty of doing it sometimes. I will be more mindful after reading this in doing null checks :)

Collapse
 
jamesmh profile image
James Hickey

Great! Yes, it's very common :)

Collapse
 
helenanders26 profile image
Helen Anderson

Great post!

I've just been reading about "The Billion Dollar mistake" from a SQL point of view, interesting to hear another perspective on the same.

Collapse
 
jamesmh profile image
James Hickey

Oh ya... NULL in SQL can be even trickier!

Collapse
 
pencillr profile image
Richard Lenkovits

This came as divine intervention. I'm currently struggling with a null check hell case because of some inconsistently structured api data, and I was thinking about what would be the clean way to deal with it. Then this article came up. Thanks! :)

Collapse
 
jamesmh profile image
James Hickey

Sweet! The book covers the special case pattern also.

Collapse
 
ifarmgolems profile image
Patrik Jajcay

If you're using JS, take a look at lodash.com/docs/#get

Collapse
 
joelvarty profile image
Joel Varty

Nicely written - a big part of this comes from creating a pattern and following it closely, especially with a team. Huge value in having code that's not only clean but easily predictable.

Collapse
 
jamesmh profile image
James Hickey

👍 Def. Having some clear standards, common patterns, etc. will help a team be on the same page. Codebases where there's no predictability or conventions are hard to maintain, etc. 😥

Collapse
 
joelvarty profile image
Joel Varty

💯

Collapse
 
moopet profile image
Ben Sinclair

I don't particularly like the NullBoss pattern.

It means you have to introduce fictional objects to work around a problem, which moves the problem elsewhere. Now you can't make a currentLevel.hasBoss function without making checks for your special object, or adding a property to it called isActuallyARealBoss.

In that sort of instance, I'd use an array, since there could be multiple bosses. As for being able to assign an empty array, I'd make sure I was using a language that allowed me to do that, because I value my mental health!

I agree that functions shouldn't generally return "thing or null".

Collapse
 
jamesmh profile image
James Hickey

One of the benefits of this pattern is that you typically would avoid the need for a hasBoss method (unless there's an exceptional case where you would need that check). Otherwise, using null objects should remove the need to do that.

I would agree that an array of bosses would work well, if the scenario warrants it.

Thanks! Great feedback.

Collapse
 
sambenskin profile image
Sam Benskin • Edited

What would the hasBoss function be used for? My understanding is the null object pattern would handle it for you. If you want current level to do something, then just do it and let the Boss or NullBoss handle what to do in each situation

Collapse
 
olvnikon profile image
Vladimir

This way of handling nullable objects reminds me a special type in Functional programming called Option (or Maybe). It keeps two values: None and Some(val). While Some always keeps a real not null value and you can map it, None has nothing but it is not null.
I would also apply this pattern instead of creating null versions of objects because it is more generic and covers all nullable cases. For example, fp-ts provides such utilities as fromNullable.

Thread Thread
 
jamesmh profile image
James Hickey

Agree. This was a solution I debated whether to include in the book or not. Due to not wanting to take forever with the book, I decided to exclude it.

But I agree, there are many situations where using the option pattern works very well 👍

Collapse
 
ronancodes profile image
Ronan Connolly 🛠 • Edited

What about using undefined instead of null?
Better or worse?

Collapse
 
emptyother profile image
emptyother

Undefined in javascript is what null is in other languages. I think its better to use it. Best is to avoid both.

But a lot of js developers expect a null. A lot of libs return null (some even return either, with no reason for one or the other). And js itself also returns null, for example getElementById.

Collapse
 
ronancodes profile image
Ronan Connolly 🛠

The reason I use it is that if you see a null you known that it was explicitly set, where as undefined means it hasn't been touched.

Would you recommend just sticking to undefined rather than mixing null and undefined?

Thread Thread
 
emptyother profile image
emptyother

Do your code ever do things differently based on if a variable contains a null or if it contains a undefined?

Let me try a bad analogy: If couldYouBuyMeSomeFlowers() returns null, it would be reasonable to assume that the florist is out of flowers (why else would he give us an explicit null instead of nothing).. But if it returns undefined.. Did the method forget to go to the florist? Should i call couldYouBuyMeSomeFlowers() again until it returns flowers or null? Or is the florist gone? No matter the reason, I still don't have flowers and I will have to throw a PartyCanceledError("Sorry, we can't have a party without flowers").

If i needed a reason why the method failed, it would be better if the method just threw an error instead. StoreNotFoundError or NotEnoughMoneyError. Or a Promise<Flower[]> for a later delivery. I could deal with that.

Collapse
 
dontry profile image
Don

Hi James, how do you instantiate the NullBoss class? I reckon it still needs to do the null check in the first place, doesn't it?

Collapse
 
jamesmh profile image
James Hickey

The NullBoss doesn't do any null checking because it replaces the concept of null altogether.

const nullBoss = new NullBoss();
nullBoss.fight(player);
// Etc.

Usually with kind of pattern, you would have a factory of some kind create your objects for you.

Does that help?

Collapse
 
dimpiax profile image
Dmytro Pylypenko

Could you provide the practical example with NullBoss?
Cause the current example is not like dummy date, that can be used as mock. But the example which is far from reality, correct me if I wrong.

Thread Thread
 
olvnikon profile image
Vladimir

I think the whole idea of using NullObject is taken from Option type, which holds None or Some. When they is no null you don't think it exists. E.g. while Some holds a real not null value, which can be mapped, None always returns None. This approach is called "Railway programming".

fromNullable(null).map(double).map(tripple); // None
fromNullable(2).map(double).map(tripple); // Some(12)

null is avoided and the program doesn't crash. Just different way of thinking when there is no null.

Thread Thread
 
dimpiax profile image
Dmytro Pylypenko

As I know, the pattern was a long time before Optionals (some, none).
And what author proposes – it’s a big difference from it.
Using optionals you stop the execution flow, while the topic about redundant objects creation.

Thread Thread
 
lukejs profile image
Luke

2 examples I've seen in Minecraft:

Air blocks - they are blocks like any other, rather than nulls to represent a block not being present in a location

Also added in the last few versions (replacing null) - "Empty" item stacks - there is a singleton "ItemStack.EMPTY" and a method "ItemStack#isEmpty()"

Collapse
 
kopseng profile image
Carl-Erik Kopseng

Like in all OOP, you still need to make the decision on which class to create somewhere. What you gain by taking the decision at a higher-up place is the ability to avoid this decision in all other places.

Collapse
 
mburszley profile image
Maximilian Burszley
Collapse
 
mchmielarski profile image
Maciej

nice post! but one typo in examples

const myEmptyArray: string[] = EmptyArray.create<T>()

should be

const myEmptyArray: string[] = EmptyArray.create<string>()
Collapse
 
jamesmh profile image
James Hickey

Good catch! Thanks

Collapse
 
bendem profile image
bendem • Edited

Sometimes null is useful but generally, it plagues the code. Null being part of the type system like typescript allows is really neat (props to Kotlin, Rust and C++ too). Having the compiler yell at you for checking for null when something can never be null makes code a lot more readable without leaving the writer wondering whether this or that will actually be null or not.

Nice article.

This part though, I don't understand:

Take 2
Other languages like C#, Java, etc. won't allow you to assign a mere empty array to a collection due to rules around strong typing (i.e. []).

In those cases, you can use something like this version of the Null Object Pattern:

class EmptyArray<T> {
    static create<T>() {
        return new Array<T>()
    }
}

// Use it like this:
const myEmptyArray: string[] = EmptyArray.create<string>();

I'm not sure what this is supposed to mean. You can make empty arrays in C# and Java just fine (though you generally use an empty collection instead of raw arrays).

String[] myEmptyArray = new String[0];

// Even better
Collection<String> myEmptyCollection = List.of();
// Or in older versions
Collection<String> myEmptyCollection = Collections.emptyList();
// Or in even older versions
Collection myEmptyCollection = Collections.EMPTY_LIST;

The code you provided just does nothing, I'm not sure what problem you tried to solve by writing it.