Zero Lock
Aug 30 2018 09:16pm | | Share

Staying Safe from Null in JavaScript

By James McGeachie

Run-time exceptions are bad news. They are regularly responsible for broken features and application crashes. This causes user frustration, which turns them away from your app and potentially costs you business.

 

Exceptions caused by null objects are extremely common in JavaScript development and are a source of many user and developer headaches. However, this is generally because of a lack of effective null handling. With a type-checking system to prevent unexpected nulls, try-catch to handle the unpreventable ones and a suitable control flow for allowed null, run-time crisis can be averted. To help prevent headaches for all of us, I’m going to discuss these approaches.

 

“Null” in JS

 

Many programming languages use the concept of a ‘null’ value to represent the absence of value. JavaScript, for better or worse, actually has 2 separate values to achieve this end, which are:

 

  • undefined - the default value of any variable that has been declared without being assigned to, and the value returned when a non-existent property is accessed
  • null - a value that can be assigned to a variable that specifically indicates an absence of value

 

I refer to both of these as null in this article for the sake of convenience as they can both result in the same category of problem.

 

Null Object Exceptions

 

How do null values cause exceptions? Let’s have a look at a trivial example:

 


let foo = {};

console.log(foo.bar.baz);

 

If we run this, we’ll see the following error message:

 

“Uncaught TypeError: Cannot read property 'baz of undefined”.

 

We created an empty object named foo, but its bar property was never assigned and thus has no object value in memory and cannot point to baz. This type of run-time error is extremely prevalent in vanilla JS development because JS is a dynamic language and allows errors like this to easily slip by as they won’t be thrown until program execution.

 

So, what can we do about these exceptions? Well, the strategies you have available will vary on a case-by-case basis depending on the answer to this question - is null an allowed value in this case or not? Can the program simply take no action on null, or is this a bug that has to be caught and fixed? Let’s look into both scenarios.

 

Disallowed Null Values

 

There are probably going to be cases in your application where you try to access a property that should never knowingly be null, i.e. if it is null, this is a bug. We can break this down into 2 sub-cases - preventable and not-preventable.

 

Preventable Nulls

 

If a disallowed null value originates in a codebase you have direct control over, then I would consider it preventable. One way to achieve this is through a type-checking system.

 

Type-checking

 

As JavaScript development has become used at increasingly larger scales, the demand for the robustness of a statically-typed language has increased. Enter type-checking solutions such as TypeScript, a superset of JS allowing static types. These require an extra compilation step to be added to your build process, but can be well-worth it if you wish to catch simple errors early. Let’s take our earlier example and simply add TypeScript interfaces.

 


interface Foo {

  bar: Bar;

}

 

interface Bar {

  baz: string;

}

 

let foo: Foo = {};

console.log(foo.bar.baz);

 

When we compile to JavaScript, we will see the following error:

 

“error TS2322: Type '{}' is not assignable to type ‘Foo'.  Property 'bar' is missing in type ‘{}’”

 

Because the value of foo does not match the implemented Foo interface, we get a compilation error and thus prevent the null lookup from ever reaching runtime.  This is just one of many benefits of TypeScript usage, and if you’re unfamiliar with the language you may wish to investigate it further.

 

Unpreventable Nulls

 

Sometimes you will be working with a dependency that cannot be exchanged for an alternative. This may be a client-side dependency such as a third-party library, or a server-side API that is maintained by another team. In these cases, you may have little to no control over what the API returns - your application is dependent on it functioning correctly free from bugs and you probably can’t know if there’s a bug until runtime.

 

So, what if there is a bug? If we have a deeply nested object structure which has an unexpected null value 3 levels down - should we have conditionals to handle the possibility of this? Should that responsibility be in your code?

 

My recommendation is that this is a valid use-case for try-catch. Rather than aiming to prevent every dependency-related exception, we can acknowledge that there is an unknown risk of exception when we use third party code and therefore ensure that we catch and handle it if it occurs.


Take the following example:

 


function getUsers() {

  return http.get(‘/users’).then(extractUsers);

}

 

function extractUsers(response) {

  return response.body._embedded.items;

}

 

You may have run into a case like this, where the server API you are consuming must first be unpacked from a raw response. The problem is that you leave yourself open to the back-end introducing a bug, or breaking change, that means one of these nested values is null.

 

I would handle this as follows:

 


function extractUsers(response) {

  try {

    return response.body._embedded.items;

  } catch(error) {

    handleError(error);

  }

}

With a try-catch, any null within that nested structure is caught and we can use a dedicated error-handling function to take care of this (possibly reporting the error in some form so the API issue can be investigated, then fall back to a ‘sad path’ on the client).

 

This is pretty convenient. So, should we use this approach all the time? Well, the trade-off for this convenience is a performance hit. To give you a sense of this, let’s do a quick benchmark of 2 simple implementations of extractUsers (note: benchmarked in Google Chome, so this is running on the V8 JavaScript engine).

 


function extractUsers(response) {

  if (response && response.body && response.body._embedded) {

    return response.body._embedded.items;

  } else {

    console.log('Could not extract users');

  }

}

 

function extractUsers2(response) {

  try {

    return response.body._embedded.items;

  } catch(e) {

    console.log('Could not extract users');

  }

}

let benchmarker = (fn, n) => {

  let startTime = new Date();

  for (let i = 0; i < n; i++) {

    fn();

  }

  let endTime = new Date();

  console.log('Duration:', endTime - startTime);

}

benchmarker(() => extractUsers({ body: null }), 100000);

Duration: 14213

benchmarker(() => extractUsers2({ body: null }), 100000);

Duration: 18847

 

In this simple scenario, we see that using the try – catch approach to guard against null results in a 4634ms longer execution time for 100,000 iterations. This result is repeatable with a similar difference.

 

So, try-catch is slower – does that mean we should avoid it? The answer to that is ‘it depends’. In this benchmark, this difference for a single iteration is a negligible 0.04634ms. In a lot of use cases you would be dealing with single iterations and thus if you had a similar implementation, performance would not be a meaningful concern. There could be cases where the performance hit is more significant. This difference was observed in a case with a simple call stack. If you have a deep call stack of nested function calls, the overhead of try-catch will be more pronounced.

 

Your decision to use try-catch should be based on your specific case. If you do not have much trust in the API you are using, have a large number of iterations and a complex call stack, perhaps the performance overhead will be too high and you would choose to use control flow instead. If performance is very unlikely to be a concern, I would recommend using try-catch.

 

Allowed Null Values

 

Null is not always necessarily a bad value to encounter. There may be cases in your logic where it makes perfect sense to allow for the null case. This puts your null handling into the domain of control flow.

 

Let’s say you have a capitalize formatter function that looks something like this:

 


 function capitalize(text) {

   return text[0].toUpperCase() + text.slice(1).toLowerCase();

 }

It requires text to be a string. The JS string type is actually an object that provides the methods being chained. Therefore, if we invoke those methods without a string, we will encounter a null value exception. However, it may be valid in this codebase for null values to be passed to capitalize. For example when iterating over a collection of text values it may be that null values are encountered. In which case, we may just want to do nothing, e.g.

 


function capitalize(text) {

  if (!text) {

    return text;

  }

 

  return text[0].toUpperCase() + text.slice(1).toLowerCase();

}

 

If you are going to be handling null conditions like this in your codebase, ensure that you also test these cases in your unit tests. It’s a good habit to always ask yourself when writing tests “What would happen if the input to this function was null?” If you don’t know the answer, there’s chance this is going to be a bug.

 

Final Thoughts

 

This article is really focused on giving you some simple ideas on how to avoid null exceptions in common JavaScript web development scenarios and is not specific to any architectural pattern. If you write applications in an object-oriented style, you may be interested in in the Null Object Pattern. Some reading material on this topic has been included in resource links below. What I stress more than anything is that null values should never be an afterthought in your development -> if you don’t consider them up front you are almost guaranteed to run into bugs. Be cautious and you’ll spare yourself, and your users, a lot of pain.

 

Resources

https://en.wikipedia.org/wiki/Null_object_pattern

http://wiki.c2.com/?NullObject

https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

https://www.typescriptlang.org/