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 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:
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.
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
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
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
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.
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.
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.
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 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: {} };
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.
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.
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 see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.