DEV Community

Almaju
Almaju

Posted on • Updated on

Why null is an abomination

Imagine you're halfway through your third cup of coffee, navigating through a sea of null in your codebase, and you can't help but whisper to yourself, "What in the world..." Perhaps you're even tempted to unleash a fiery comment into the void of the internet. It’s widely known that Tony Hoare, the architect behind the null reference back in 1965, has famously labeled it his "billion-dollar blunder."

Humorous GIF depicting frustration with null references

Let’s dive into the intricacies of null through the lens of TypeScript, a language that you all know (and hate?).

Unraveling the Mystery of null

At its core, null signifies the intentional absence of a value. Its sibling, undefined, serves a similar role, denoting an uninitialized state. Here’s a simple breakdown:

  • undefined: A variable not yet blessed with a value.
  • null: A conscious decision to embrace emptiness.

Consider this example:

type User = {
  username: string,
  pictureUrl?: string,
};

type UpdateUser = {
  username?: string,
  pictureUrl?: string | null,
};

const update = (user: User, data: UpdateUser) => {
  if (data.pictureUrl === undefined) {
    // No action required
    return;
  }
  if (typeof data.pictureUrl === "string") {
    // Update in progress
    user.pictureUrl = data.pictureUrl;
  }
  if (data.pictureUrl === null) {
    // Time to say goodbye
    delete user.pictureUrl;
  }
}
Enter fullscreen mode Exit fullscreen mode

This scenario is common when working with partial objects from APIs, where undefined fields imply no change, and null explicitly requests data deletion.

However, how clear is it that null means "I am going to delete something"? You better hope that you have a big warning banner in your documentation and that your client will never inadvertently send a null value by mistake (which is going to happen by the way).

It feels natural because you have "NULL" in your database but employing null in your programming language solely because it exists in formats like JSON or SQL isn’t a strong argument. After all, a direct one-to-one correspondence between data structures isn't always practical or necessary. Just think about Date.

The trouble with null

The introduction of null into programming languages has certainly stirred the pot, blurring the lines between absence and presence. This ambiguity, as demonstrated earlier, can lead to unintuitive code. In fact, the TypeScript team has prohibited the use of null within their internal codebase, advocating for clearer expressions of value absence.

Moreover, the assumption that null and undefined are interchangeable in JavaScript leads to bewildering behavior, as illustrated by the following examples:

console.log(undefined == null); // Curiously true
console.log(undefined === null); // Definitely false
console.log(typeof null); // Unexpectedly "object"
console.log(typeof undefined); // As expected, "undefined"
console.log(JSON.stringify(null)); // Returns "null"
console.log(JSON.stringify(undefined)); // Vanishes into thin air
Enter fullscreen mode Exit fullscreen mode

Regrettably, I've written code like this more often than I'd like to admit:

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}
Enter fullscreen mode Exit fullscreen mode

Furthermore, if you begin incorporating both null and undefined into your codebase, you will likely encounter patterns like the following:

const addOne = (n: number | null) => (n ? n + 1 : null);

const subtractOne = (n?: number) => (n ? n - 1 : undefined);

const calculate = () => {
  addOne(subtractOne(addOne(subtractOne(0) ?? null) ?? undefined) ?? null);
};
Enter fullscreen mode Exit fullscreen mode

The undefined conundrum

Okay, so we just ban null from our codebase and only use undefined instead. Problem solved, right?

It’s easy to assume that optional properties {key?: string} are synonymous with {key: string | undefined}, but they’re distinct, and failing to recognize this can lead to headaches.

const displayName = (data: { name?: string }) =>
  console.log("name" in data ? data.name : "Missing name");

displayName({}); // Displays "Missing name"
displayName({ name: undefined }); // Displays undefined
displayName({ name: "Hello" }); // Greets you with "Hello"
Enter fullscreen mode Exit fullscreen mode

Yet, in other scenarios such as JSON conversion, these distinctions often evaporate, revealing a nuanced dance between presence and absence.

console.log(JSON.stringify({})); // {}
console.log(JSON.stringify({ name: undefined })); // {}
console.log(JSON.stringify({ name: null })); // {"name": null}
console.log(JSON.stringify({ name: "Bob" })); // {"name":"Bob"}
Enter fullscreen mode Exit fullscreen mode

So, sometimes there are differences, sometimes not. You just have to be careful I guess!

Another problem of undefined, is that you often end up doing useless if statements because of them. Does this ring a bell?

type User = { name?: string };

const validateUser = (user: User) => {
  if (user.name === undefined) {
    throw new Error('Missing name');
  }

  const name = getName(user);

  // ...
}

const getName = (user: User): string  => {
  // Grrrr... Why do I have to do that again?
  if (user.name === undefined) {
    throw new Error('Missing name');
  }

  // ... and we just created a potential null exception
  return user.name!;
}
Enter fullscreen mode Exit fullscreen mode

Consider this scenario, which is even more problematic: a function employs undefined to represent the action of "deleting a value." Here's what it might look like:

type User = { id: string, name?: string };

const updateUser = (user: User, newId: string, newName?: string) => {
  user.id = newId;
  user.name = newName;
}

updateUser(user, "123"); // It's unclear that this call actually removes the user's name.
Enter fullscreen mode Exit fullscreen mode

In this example, using undefined ambiguously to indicate the deletion of a user's name could lead to confusion and maintenance issues. The code does not clearly convey that omitting newName results in removing the user's name, which can be misleading.

A path forward

Instead of wrestling with null-ish values, consider whether they’re necessary. Utilizing unions or more descriptive data structures can streamline your approach, ensuring clarity and reducing the need for null checks.

type User = { id: string };

type NamedUser = User & { name: string };

const getName = (user: NamedUser): string => user.name;
Enter fullscreen mode Exit fullscreen mode

You can also have a data structure to clearly indicate the absence of something, for example:

type User = {
  id: string;
  name: { kind: "Anonymous" } | { kind: "Named", name: string }
}
Enter fullscreen mode Exit fullscreen mode

If we jump back to my first example of updating a user, here is a refactored version of it:

type UpdateUser = {
  name:
    | { kind: "Ignore" }
    | { kind: "Delete" }
    | { kind: "Update"; newValue: string };
};

Enter fullscreen mode Exit fullscreen mode

This model explicitly delineates intent, paving the way for cleaner, more intuitive code.

Some languages have completely abandoned the null value to embrace optional values instead. In Rust for example, there is neither null or undefined, instead you have the Option enum.

type Option<T> = Some<T> | None;

type Some<T> = { kind: "Some", value: T };

type None = { kind: "None" };
Enter fullscreen mode Exit fullscreen mode

I recommend this lib if you want to try this pattern in JS: https://swan-io.github.io/boxed/getting-started

Conclusion

Adopting refined practices for dealing with null and undefined can dramatically enhance code quality and reduce bugs. Let the evolution of languages and libraries inspire you to embrace patterns that prioritize clarity and safety. By methodically refactoring our codebases, we can make them more robust and maintainable. This doesn't imply you should entirely abandon undefined or null; rather, use them thoughtfully and deliberately, not merely because they're convenient and straightforward.

Top comments (11)

Collapse
 
lnahrf profile image
Lev Nahar • Edited

The horrors described in this (wonderful, mind you) article only exist in TypeScript.

Oh G-d, how I hate TypeScript.

I will present the antithesis of TypeScript and say Dart, is a null-safe language, in which variables must be declared as nullable. The compiler prevents you from using a nullable value without checking if its empty first, which reduces bugs significantly and makes it so you will only use nullable variables when you have to (e.g. dynamic server response).

Most languages are not built to break like Typescript. Unfortunately people still use Typescript.. this devastating fact caused you to write this article.

When coding Javascript, I try to avoid undefined as much as I can and use null when an empty value is required. This brings Javascript up to par with nicer languages (like Dart, I love Dart, it’s truly great).

Collapse
 
htho profile image
Hauke T.

IMO using undefined (instead of null) makes TS feel more like a Null-Safe language. The compiler makes sure you check for undefined values.
Yes the compiler will force you to check for null as well, but you can't avoid undefined in JS it is everywhere, missing properties, missing arguments, everywhere.
I prefer to only use one of both, and it's definitely not null.

Just make the compiler strict and it will annoy you as much as you need to write good code.

Collapse
 
codenameone profile image
Shai Almog

Should be "Why null in JavaScript is an Abomination".

I'd argue that the workarounds for null are far worse than the problem. E.g. in a consistent language like Java null is far less of a problem.

Collapse
 
citronbrick profile image
CitronBrick

I saw the title, & thought that the article was about using "null" instead of null & expecting it to work like null.

Collapse
 
latobibor profile image
András Tóth

Fascinatingly, I rarely struggle with null. When I do, usually the fix is straightforward as I have an error stack, I put in a console.log see what went wrong and fix it in no time. I quickly forget about null related errors.

For me the trillion dollar problem is swallowing errors. try {} catch { // do nothing }. That is way, waaaay bigger of a problem as now everything works until it does not, but you have no error stack, no pointer, nothing where the problem occurred.

The first version of Angular infamously made the decision to swallow any errors coming from the template, wasting thousand of hours of my colleagues and me. I still remember the pain of accidentally writing my-value="string-value" instead of my-value="'string-value'"; no error messages, no warnings, nada.

A good ole null error? As I said, 15 minutes fix. Unless it is coming from something swallowing an error in the chain beforehand.

Collapse
 
bwca profile image
Volodymyr Yepishev

I don't think it is an abomination, most developers just misuse it as it is a way to cut corners, since you can switch off strict null checks in ts and throw null into anything.

Collapse
 
jmfayard profile image
Jean-Michel 🕵🏻‍♂️ Fayard

As Tony Hoare the problem is not null which is itself perfectly fine.
The problem was that it wasn't part of the typesystem.
In a modern programming language which has null in the type system, null is great, in fact it's better than the alternatives of using an Option Monad or similar

Collapse
 
almaju profile image
Almaju

By curiosity, why do you think it's better than the Option alternative?

Collapse
 
jmfayard profile image
Jean-Michel 🕵🏻‍♂️ Fayard
Collapse
 
htho profile image
Hauke T.

I avoid null in JS/TS. If there is an API that returns null, I immediately coalese to undefined:

const el = document.getElementById("some id") ?? undefined;
Enter fullscreen mode Exit fullscreen mode

From this point the compiler forces me to check if the value is undefined.

I only struggle with null/undefined values in legacy code.

Collapse
 
gorhovakimyan200121 profile image
gorhovakimyan200121

The concept that "null is an abomination" I can explain with the idea that handling null values in programming can lead to errors, bugs, and unexpected behavior if not managed properly.

The problem arises when developers don't appropriately check for null values before using them, leading to exceptions that can crash programs or cause unexpected behavior. These errors can be particularly tricky to debug because they often occur at runtime rather than compile time. To avoid these issues, developers are encouraged to handle null values explicitly in their code, either by checking for null before using a variable or by using techniques like defensive programming, where methods and functions are designed to handle unexpected inputs gracefully.