Faraz Kelhini JavaScript developer.

Error-free property chaining with ES2020 optional chaining operator

5 min read 1678

JavaScript logo.

In JavaScript, accessing a deeply nested property often involves checking whether each property in the chain is valid.

The logic behind this strategy is simple: if one of the properties evaluates to null or undefined, the code throws a TypeError. null and undefined are primitive values that cannot have any properties.

So, it shouldn’t come as a surprise that treating these values as objects is problematic.

In this article, we’ll first look at the existing ways of dealing with property chains in JavaScript and then see how the optional chaining operator streamlines the process and improves the readability of the code with a shorter, more intuitive syntax.

The problem

The best way to understand why it might not be safe to access a nested property directly is through an example. Suppose you want to use a web service to retrieve the current time for Tokyo, Japan. And the service returns a JSON response like this:

{
    "data": {
        "datetime": "2020-06-26T21:04:47.546298+09:00",
        "day_of_week": 5,
        "day_of_year": 178,
        "timezone": "Asia/Tokyo",
        "utc_datetime": "2020-06-26T12:04:47.546298+00:00",
        "utc_offset": "+09:00",
        "week_number": 26
    }
}

You’re only interested in the value of the datetime property, so you assign it to a variable to process it:

const datetime = response.data.datetime

But, what if the API changes the structure of the response and the property you’re looking for is no longer available at response.data.datetime?

That would cause an error like this: TypeError: Cannot read property 'datetime' of undefined.

To write less error-prone code, JavaScript developers usually check for the existence of each property in the chain, like this:

We made a custom demo for .
No really. Click here to check it out.

let datetime;
const response = {
    //…
};
 
if (response && response.data) {
    datetime = response.data.datetime;
}

This code ensures that response and response.data are non-null and non-undefined properties before accessing the value of response.data.datetime.

Another way to achieve this is by using the ternary operator:

const response = {
    //…
};

const datetime =
    (response ?
        (response.data ?
            response.data.datetime :
            undefined) :
        undefined);

Both of these approaches seem hacky and affect the readability of the code, especially if the property is deeply nested. Fortunately, there’s now a better way to deal with this pesky problem.

Introducing the optional chaining operator

The optional chaining operator is an ES2020 proposal that provides a straightforward syntax for accessing a nested property without the need to explicitly check that each object in the chain exists.

This proposal is currently at stage 4, which means it’s ready for inclusion in the JavaScript specification. The good news is that all modern browsers, including Chrome 80+, Firefox 74+, and Safari 13.1+, have already implemented the feature.

To use the optional changing operator, precede a chain of one or more property accesses with the ?. token. Here’s an example:

const obj = {};
const city = obj?.user?.address?.city;
 
console.log(city);    // => undefined

This code attempts to access a nested property that doesn’t exist. But JavaScript returns an undefined value rather than throwing an error. As you can see, the syntax is not only shorter but also more readable.

Technically, obj?.user is equivalent to obj == null ? undefined : obj.user. The ?. token simply provides us with a shortcut.

Keep in mind that you cannot use the optional chaining operator on the left-hand side of an assignment. Attempting to do so results in a SyntaxError:

const obj = {};

obj?.property = 123;    // => SyntaxError: Invalid left-hand side in assignment

Optional method calls

There’s also a version of the optional chaining operator that’s useful when calling an object’s method that may not exist. Consider this example:

const obj = {};
const value = obj.undefinedMethod?.();
 
console.log(value);    // => undefined

Here, obj.undefinedMethod?.() attempts to call a method that isn’t defined. But because the expression uses the ?.() token, it returns undefined.

Without the optional chaining operator, this code would throw an error:

const obj = {};
const value = obj.undefinedMethod();    // => TypeError: obj.undefinedMethod is not a function
 
// the console.log() method won’t have a chance to run
console.log(value);

Keep in mind that there are some special cases where ?. throws an error instead of returning undefined.

For instance, if you attempt to access a method that doesn’t exist, but the object has a property with the same name, then a TypeError will occur:

const user = {
    name: "Joe"
};
 
const value = user.name?.();    // => TypeError: user.name is not a function

Also note that the result of obj.a?.().x is completely different from the result of obj.a()?.x. The former returns undefined if obj.a() doesn’t exist, or obj.a has a value of null or undefined.

The latter, on the other hand, returns undefined if obj.a() returns anything other than an object containing an x property.

You can use it, for example, to retrieve the value of an HTML element that may not exist:

// querySelector() returns null if the element doesn't exist on the page

const elem = document.querySelector('.abc')?.innerHTML;
// No error. elem will have a value of undefined

const elem = document.querySelector('.abc').innerHTML;
// => TypeError: Cannot read property 'innerHTML' of null

Optional dynamic property access

There’s one more variant of the optional chaining operator: ?.[]. This token is useful when
accessing an object’s property using the bracket notation. Let’s look at an example:

const obj = {
    user: {
      name: "joe"
    }
};
 
const value = obj?.user?.address?.["city"];
 
console.log(value);    // => undefined

This code attempts to access the value of the city property. But because user doesn’t have a property named address, it returns undefined. Compared to regular property access, this is less error-prone:

const obj = {
    user: {
        name: "joe"
    }
};
 
const value = obj.user.address["city"];    // => TypeError: Cannot read property 'city' of undefined

Another advantage of this syntax is the ability to use dynamically-generated property names. For example:

const config = {
    darkMode: {
         default: 0,
         state: 1
    },
    notifications: {
        default: 1,
        state: 0
    }
};
 
const option = 'darkMode';
const state = config?.[option].state;
 
console.log(state);    // => 1

But what about array items? Can we use the optional chaining operator to access array elements safely? The answer is yes:

const arr = null;
let index = 2;
let item = arr?.[index];
 
console.log(item);    // => undefined

Using the optional chaining operator with the nullish coalescing operator

As with the optional chaining operator, the nullish coalescing (??) operator is a stage 4 ES2020 proposal that’s already implemented by all modern browsers.

This operator acts very similar to the logical OR (||) operator, except that it doesn’t work based on whether the value is truthy. Instead, the result of the operator depends on whether the value is nullish, which means null or undefined.

So, in the expression, a ?? b, the resulting value is b only if a evaluates to undefined or null.

Compare the following:

false || true;    // => true
false ?? true;    // => false
 
0 || 1;           // => 1
0 ?? 1;           // => 0
 
null || [];       // => []
null ?? [];       // => []
 
undefined || [];  // => []
undefined ?? [];  // => []

Now, we can combine the nullish coalescing operator with the optional chaining operator when we desire some value other than undefined for a missing property.

For example:

const config = {
    general: {
        language: null
    }
};
 
const language = config?.general?.language ?? "English";

console.log(language);    // => English

This code sets English as a default value for config.general.language. So, when the property is undefined or null, the default value will be used.

Short-circuit evaluation

An interesting aspect of the optional chaining operator is its ability to be used in short-circuit evaluations. That means if an optional chaining operator returns early, the rest of the expression won’t be evaluated. Consider the following code:

const obj = null;
let a = 0;
 
obj?.[++a];
 
console.log(a);    // => 0

In this example, a is not incremented because obj has a null value.

This code is equivalent to:

const obj = null;
let a = 0;
 
obj == null ? undefined : obj[++a];
 
console.log(a);    // => 0

The important point to remember is that when short-circuiting occurs, JavaScript ignores the expression following the optional chaining operator.

Limiting the scope of short-circuiting

As we learned, we can use short-circuiting to skip the rest of an expression. But, is it possible to limit the scope of that? As with any expression in JavaScript, we can use the grouping operator ( ) to control the evaluation:

(obj?.user).name;

In practice, however, it’s hard to find a real-world use case or compelling reason to use this feature.

Optional deletion

Another interesting characteristic of the optional chaining operator is that you can use it in conjunction with the delete operator:

const obj = null;
 
// no error.
// even though obj.user doesn’t exist.
delete obj?.user;    // => true
 
// equivalent to
// obj == null ? true : delete obj.user

Notice how the delete operator returns true despite not deleting anything from obj. Without the optional chaining operator, the code would throw a TypeError:

const obj = null;
     
delete obj.user;    // => TypeError: Cannot convert undefined or null to object

Stacking

Stacking is just a fancy name for the ability to use more than one optional chaining operator on a sequence of property accesses.

When stacking, you should ask yourself whether a property ever has a chance of containing a nullish value. If it doesn’t, then there’s no reason to apply the optional chaining operator.

Take the following object as an example. If the data property is always guaranteed to exist and contain a non-nullish value, then you shouldn’t use optional chaining:

const obj = {
    data: {}
};

Prior art

For developers coming from C#, Swift, or CoffeeScript, the optional chaining operator is nothing new. A similar feature has long existed in those languages.

In fact, JavaScript has formed the general semantics of the optional chaining operator by imitating those languages.

There are also some languages such as Kotlin, Dart, and Ruby that provide a similar feature but with one crucial difference: they do not short-circuit the entire property chain when it’s longer than one element.

Conclusion

The optional chaining operator provides a robust and yet concise way of writing safer code.

Although it’s not formally a JavaScript feature yet, browsers have already begun to implement it, and the JavaScript community seems to have welcomed this new addition to the language.

If you have any questions feel free to ask in the comments, I’m also on Twitter.

: Debug JavaScript errors easier by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Faraz Kelhini JavaScript developer.

Leave a Reply