Optional Chaining in JavaScript

Until ES2020, if you wanted to access nested properties of an object in JavaScript, you had to check for null or undefined at every level, or you would end up with a TypeError.

To avoid a TypeError you would have had to create temporary variables or do a series of incremental && calls, which is pretty ugly and uses both space and time.

For example:
const obj = {
prop: {
a: "value"
}
}
// before ES2020 - no checks
obj.foo.a // TypeError: Cannot read property 'a' of undefined
// before ES2020 - incremental nested checks
obj.foo && obj.foo.b // undefined

Thanks to the new Optional Chaining lookahead operator ?. in ES2020, now we can do these checks inline.

Optional chaining - Basic usage

The syntax is simple and maybe a bit awkward at first. Just add a question mark before the next accessor dot.

Here's Optional Chaining in action, using our previous example:

const obj = {
prop: {
a: "value"
}
}
// Optional Chaining
obj.foo?.a // undefined

As you can see the left hand side of the optional chaining operator always returns undefined if the expressions returns either null or undefined.

That's really all there is to it.

Looking under the hood

So what is actually happening in optional chaining? I used the Babel REPL and examined what the transpiled code looks like.

Let's figure out what is actually going on under the hood.

Using this input:

const obj = {}
obj.foo?.a

We get this output:

var _obj$foo;
const obj = {};
(_obj$foo = obj.foo) === null || _obj$foo === void 0 ? void 0 : _obj$foo.a;

Let's replace the void 0 bit with undefined, and clean up the variable names. We then get a more human readable piece of code.

Babel creates a temporary variable pointing to the object key, compares it with both null and undefined and finally returns the resulting value if all those tests pass.

var foo;
const obj = {};
(foo = obj.foo) === null || foo === undefined ? undefined : foo.a;

The following are additional use cases for Optional Chaining.

Optional chaining - Methods and Arrays

Optional chains work on methods and arrays as well as static properties. The syntax is pretty much the same. Just remember place the . after the ? but before invoking the array or function. The syntax looks like this: obj.foo?.() or obj.array?.[x]

Here's a an example:

const obj = {
list: [1,2,3],
func: () => console.log('Hi'),
}
// before ES2020 - no checks
obj.bar[1] // TypeError: Cannot read property '1' of undefined
obj.baz() // TypeError: obj.baz is not a function
// before ES2020 - incremental nested checks
obj.bar && obj.bar[1] // undefined
obj.baz && obj.baz() // undefined
// Optional Chaining
obj.bar?.[1] // undefined
obj.baz?.() // undefined

Important Note about Top Level access

It's important to know your top level object has to exist when using optional chaininig. Meaning, in our previous case the obj itself has to actually exist before you can look ahead in the optional chain.

For example the code below will throw an error. Note that doesnNotExist does not exist in our context. So even with using optional chaining, we get an error.

doesnNotExist?.property // ReferenceError: doesnNotExist is not defined

Now, if the variable was actually instantiated, it is fair game, even if it is currently null, undefined, or of the wrong type, such as a number etc. See examples below:

const a = null;
a?.property // undefined
// As long as the first item is not undefined or null we're in good shape
const d = 24;
d.wut?.(); // undefined

Finally, if the parent is null or undefined, a second optional chain is required to protect against type errors. See the below examples:

// function call
const b = null;
b.func?.() // TypeError: Cannot read property 'func' of null
// Notice the ?. after b
// If we add an additional optional chain we can safegaurd against b being null
b?.func?.() // undefined
// array use
// Same thing, we can safegaurd against b being undefined using the first ?.
const c = undefined;
c.array?.[100] // TypeError: Cannot read property 'array' of undefined
c?.array?.[100] // undefined

Based on the abve logic, my reccommendation is to always add an additional ?. when you are calling a method or array, if it is being called on a top level object. So in summary:

// good idea
topLevel?.func?.()
topLevel?.arr?.[1]
// potentially could throw an error if topLevel is null or undefined
topLevel.func?.()
topLevel.arr?.[1]

Short Circuiting

Say you want to evaluate an additional piece of code, if your lookup does not return undefined. You can do that all in the same expression. Here's an example of short circuiting.

const data = {
list: [1,2,3]
}
// Short Circuiting
data.list?.reverse() // [3, 2, 1]
data.items?.reverse() // undefined

Long Short Circuiting

What's great is you can nest your optional chains referred to as long short-circuiting, great for deep nested objects. Nesting optional chains in this case would look like this:

obj?.parent?.child?.name // undefined

Stacking

You can stack and chain as many optional chains as you want.

one?.two?.[2].three?.(3)?.four

Unsupported operations

Don't go trying to optional chain all the things! Yes, as cool as it is, not everything under the ECMA sun can be optionally chained together. Basically, just stick to the 3 supported operations: property accessors, arrays, and function calls.

Here is a list of operations not supported in optional chaining.

// Delete
delete a.?b // ReferenceError: Can't find variable: a
// Constructor
new a?.() // SyntaxError: Cannot call constructor in an optional chain.
// Template literals
a?.`string` // SyntaxError: Cannot use tagged templates in an optional chain.
// Property assignment
a?.b = c // SyntaxError: Left side of assignment is not a reference.
// Imports, Exports, Super, etc. etc.

Oh by the way, if you're looking for assignment, take a look at the Nullish Coalescing ?? operator. I kept that out of this blog post but will write a separate one on it next.

Real world examples of Optional chaining

How is all this helpful in the real world?

While these are trivial examples, this happens a lot in JavaScript, and even more in the context of a web browser. Form values, DOM object properties, API results, methods on objects, the list goes on. We are always having to safeguard when accessing nested properties.

Here's a more real world example. Say your API is returning customer data. The data may or may not exist, and it may or may not contain an orders array, but if it does you want to render those items in a list.

Before optional chaining you would have to do something like this:

if (customer && customer.orders) {
customer.orders.map(order => printOrder(order))
}

Now you can look ahead and inspect the object returned from your API response, before doing additional work.

customer?.orders?.map(order => printOrder(order))

You can use optional chaining to check for existence of DOM elements or React Components before accessing their properties or calling their methods.

For example, say you want to run a callback method on a prop in a React component, and you're not guaranteed to have it provided. Now it's just a matter of tacking on a question mark and a dot ?. before calling the callback method.

props.onClick?.()

Here's another somewhat trivial example. Say you're adding an event listener to the window object but you don't exactly know if window object exists, maybe you're doing sever side rendering and could be in node.

window?.addEventlistener.('click', () => {});

Say you want to set attributes on some elements but not sure if they exist. Just use optional chaining.

document?.querySelector('[data-foo]')?.setAttribute('disabled', 'true');

Or maybe you are trying to access an experimental API that may not be supported in all browser yet. Say you want to use the replaceAll array method which is currently only supported in Safari. Using optional chaining you can avoid errors.

"pool".replaceAll?.("o", "e") // undefined or peel

Start using optional chaining in your projects

Optional chaining is already supported in most modern browsers. To be safe and backwards compatible, use a transformer. If you are using Babel, install the the OC plugin...

npm install @babel/plugin-proposal-optional-chaining

...and add it to your .babelrc plugins section.

{
"plugins": [
"@babel/plugin-proposal-optional-chaining",
]
}

Read more about Optional Chaining

Since optional chaining is now at stage 4, its documentation can be viewed at ECMAScript. Also check out the final TC39 proposal for Optional Chaining and the documentation on Optional Chaining at MDN for more details.

Nullish Coalescing

A very close relative of Optional chaining is Nullish Coalescing used in assignment expressions. Look here for a post on Nullish Coalescing next week.