logo image Faisal Rahman
ID EN
My profile picture taken in the summer Faisal Rahman

Optional Chaining in JavaScript


ES2020 introduced the Optional Chaining feature, which we previewed in Working with Absence in JavaScript (Part 2). This time, we will dive deeper into this new feature.

Motivation for Optional Chaining

JavaScript will throw a TypeError if we try to access a property of undefined or null. This makes sense, but if we are working with objects that have a nested structure, this behavior becomes something important to pay attention to.

let obj = {};

obj.a; // undefined
obj.a.b; // TypeError: Cannot read property 'b' of undefined

With the typeof operator and the logical && operator, we can ensure whether accessing an object’s property is safe.

let obj = {};

typeof obj.a !== "undefined" && obj.a !== null && obj.a.b; // false, but no error

Going further, we can use the logical || operator to set a fallback value.

let obj = {};

(typeof obj.a !== "undefined" && obj.a !== null && obj.a.b) ||
  "no value";
// "no value"

But isn’t that syntax too long? Long syntax using operators not designed for the use case at hand opens the door to bugs, and hurts code readability. Optional Chaining was introduced to solve this problem.

Using Optional Chaining

Syntax

Optional Chaining in JavaScript has the syntax of a question mark followed by a dot (?.). There are three places where the Optional Chaining syntax can be used:

How Optional Chaining Works

The Optional Chaining operator requires two expressions as operands, one on the left-hand side (LHS) and one on the right-hand side (RHS) of the operator (e.g. lhs?.rhs). If the LHS is nullish, the Optional Chaining operation will immediately return undefined and the RHS expression will not be evaluated and will not throw an error.

const obj = {};

obj.a?.b(); // undefined
obj.a?.[++x]; // undefined
obj.a?.b.c().d?.[e].f; // undefined

This behavior is called short-circuiting. With this behavior, no matter how long the RHS operand is, it will not be evaluated if the LHS is nullish, so it will not cause an error.

Caveat

It is very important to note that when the LHS is not nullish, the RHS will be evaluated, so Optional Chaining should be applied at the points where we think a nullish value might occur, even within the RHS of the first Optional Chaining operator.

const obj = { a: 42 };

obj.a?.length.toString(); // TypeError

In the example above, obj.a is not nullish, so obj.a.length is evaluated as undefined, and then a TypeError occurs because undefined does not have a toString property. To be safer, we should change it to:

obj.a?.length?.toString();

Why not make a single Optional Chaining operation cover the entire expression, so that if any nullish value appears in the RHS it returns undefined instead of TypeError?

That design was made intentionally so that developers can explicitly indicate branching in their code, since the operator fundamentally works as an if-else branch. Code becomes easier to debug when an error occurs because of this explicitness.

Optional Chaining Use-Case Examples

Static Property Access

Say you are working with an API to retrieve user data as shown below, and you want to get the user’s postal code.

{
    "name": "John Doe"
    "address": {
        "province": "DKI Jakarta",
        "city": "Jakarta Selatan",
        "zip_code": 12510
    }
}

However, if the user has not set an address, the API will return null for the address attribute. You can use Optional Chaining to safely access zip_code.

const zipCode = response.address?.zip_code;

Array Element and Dynamic Property Access

Using the same example, but this time the user has a shopping cart cart attribute in the form of an array, and you want to get the first item in that shopping cart.

{
    "name": "John Doe"
    "cart": [
        {
            "id": 1,
            "name": "Magic Keyboard"
        }
    ]
}

However, if the user has not added any items to the cart at all, the value of cart will be null. You can do this as follows.

const firstItem = response.cart?.[0];

Or, say you want to pick a random item from the cart to give a surprise discount, you can use the dynamic property access operator.

const item =
  response.cart?.[Math.round(Math.random() * (response.cart.length - 1))];

Function or Method Access

Still with the same API example, you know that the cart property can be of type array or have a value of null. You want to sum the prices of all items in the user’s shopping cart. You can do this safely with the help of Optional Chaining on the function call.

const total = response.cart?.reduce?.(
  (subtotal, item) => subtotal + item.price,
  0,
);

That way, if cart does not have a reduce method, this expression will return undefined.

One thing to note about this use case: the Optional Chaining operator remains consistent with how it works. When the name of the function being called is not nullish, even if it is not a function, it will still be called. The operator does not check whether the attribute being called is a function or not. The risk of a TypeError remains.

const obj = { a: 42 };

obj.a?.(); // TypeError: obj.a is not a function

Collaboration with the Nullish Coalescing Operator

Of course, returning undefined is not the ideal answer for every use case of this expression. We need a way to set a fallback return value, which is provided by the Nullish Coalescing Operator (??).

The Nullish Coalescing Operator will evaluate and return the expression on the right side if the left-hand operand is nullish.

const streetName = response.address?.street_name ?? "No address set";

A behavior worth noting about this operation is that it is also short-circuiting — if the left operand is nullish, the expression on the right operand will not be evaluated at all.

const counter = 0;
const obj = { counter: 1 };

obj.counter++ ?? ++counter;

console.log(obj.counter); // 2
console.log(counter); // 0

What the Optional Chaining Operator Does Not Do

There are several Optional Chaining expressions that were considered by the TC39 team but decided not to be supported due to a lack of convincing real-world use cases, or other reasons. Here are a few examples.


References and Further Reading