Optional chaining '?.'

Optional chaining ?. lets you reach into a nested object without blowing up when one of the links in the chain is missing. If the value on the left of ?. is null or undefined, the whole expression stops and hands you undefined instead of throwing.

The “non-existing property” problem

If you’re early in your JavaScript journey, you might not have hit this wall yet. You will. It comes up constantly the moment your data stops being perfectly shaped.

Picture a set of user objects that store information about the people using your app. Most users filled in an address, so you can read user.address and then user.address.street. But some skipped it.

Now you ask for the street of a user who never gave one:

let user = {}; // a user without an "address" property

alert(user.address.street); // Error!

That crash is JavaScript behaving exactly as designed. user.address is undefined, and reading .street off undefined is not allowed, so you get a TypeError.

user
{}
.address →
undefined
.street →
TypeError
Reading a property off undefined is where the chain breaks.

In plenty of real situations you’d rather get undefined back (read: “no street here”) than a thrown error.

The same shape shows up in the browser. A call like document.querySelector('.elem') hands you the matching element, or null when nothing matches.

// document.querySelector('.elem') is null if there's no matching element
let html = document.querySelector('.elem').innerHTML; // error if it's null

When the element isn’t there, .innerHTML is being read off null, and that throws too. Sometimes a missing element is perfectly normal, and you’d be happy for html to just come out as null or undefined.

So how do you guard against this?

The old ways, and why they grate

The direct fix is to check the value with an if or a ternary ? before you reach for the property:

let user = {};

alert(user.address ? user.address.street : undefined);

No error. But user.address is written twice, which is noise. Now do it for the query selector:

let html = document.querySelector('.elem') ? document.querySelector('.elem').innerHTML : null;

Here the search runs twice. That’s wasteful and easy to get out of sync if you edit one copy and not the other.

Go one level deeper and it gets genuinely hard to read. Say you want user.address.street.name:

let user = {}; // user has no address

alert(user.address ? user.address.street ? user.address.street.name : null : null);

Nested ternaries like this are a headache to parse even for the person who wrote them.

The && operator cleans it up a little:

let user = {}; // user has no address

alert( user.address && user.address.street && user.address.street.name ); // undefined (no error)

AND-ing the path works because && stops at the first falsy link, so you never read a property off undefined. Still, every segment of the path is spelled out again and again — user.address shows up three times up there.

This repetition is the itch that optional chaining was built to scratch.

Optional chaining

?. halts evaluation the instant the value in front of it is undefined or null, and yields undefined.

Read value?.prop like this:

  • if value exists, it behaves as plain value.prop;
  • otherwise (value is null or undefined) the expression is undefined, full stop.

Here’s the address example, made safe:

let user = {}; // user has no address

alert( user?.address?.street ); // undefined (no error)

Short, clean, no property name written twice.

user
?.address
undefined
?.street
skipped
undefined
?. checks each link; the first missing one returns undefined and skips the rest.

The DOM version collapses to a single line, and the selector only runs once:

let html = document.querySelector('.elem')?.innerHTML; // undefined if there's no element

It even survives the topmost value being missing. user?.address is fine when user itself is null:

let user = null;

alert( user?.address ); // undefined
alert( user?.address.street ); // undefined

Notice the second line: user?.address.street doesn’t throw even though there’s a plain .street after address. That’s the short-circuit at work — because user is null, the whole chain bails out before it ever reaches .street.

?. only guards the value right before it

This is the part people misread. The ?. makes optional exactly one thing: the value immediately to its left. Nothing further along the chain gets protected automatically.

In user?.address.street.name, the ?. lets user be null or undefined safely. But if user exists and address turns out to be missing, .street is read off undefined and you’re back to a crash. To guard those links too, swap their . for ?.:

// guards user AND address
user?.address?.street?.name
user?.address.street← plain dot, not guarded
user?.address?.street← every hop guarded
One ?. protects one hop. Put ?. everywhere a value might be missing.

Short-circuiting

Once ?. sees that its left side doesn’t exist, it stops right there — and “stops” means everything to the right is skipped, including any function calls or side effects.

let user = null;
let x = 0;

user?.sayHi(x++); // no "user", so sayHi is never called and x++ never runs

alert(x); // 0, x was not incremented

Because user is null, evaluation short-circuits before reaching sayHi, so the x++ argument is never evaluated either. x stays at 0. This is worth remembering when the skipped part has side effects — they simply don’t happen.

user = null
?.
sayHi(x++)
never runs · x stays 0
With user null, evaluation stops at ?. and never touches the call or its arguments.

Other forms: ?.() and ?.[]

?. isn’t really an operator in the usual sense — it’s a syntax construct, and it comes in a few flavors that also cover function calls and bracket access.

?.() calls a function that might not be there.

Say some users have an admin method and others don’t:

let userAdmin = {
  admin() {
    alert("I am admin");
  }
};

let userGuest = {};

userAdmin.admin?.(); // I am admin

userGuest.admin?.(); // nothing happens (no such method)

Look closely at what’s guarded. Both lines start with a plain dot: userAdmin.admin and userGuest.admin. That’s deliberate — you’re assuming the user object itself exists, so reading a property off it is safe. The ?.() then checks the thing it’s about to call: if admin is a function, it runs; if admin is missing (undefined), the call is skipped without error.

?.[] is the bracket-notation cousin. Use it when you access properties with [] instead of a dot, for the same “the object might not exist” reason:

let key = "firstName";

let user1 = {
  firstName: "Maya"
};

let user2 = null;

alert( user1?.[key] ); // Maya
alert( user2?.[key] ); // undefined

?. pairs with delete too:

delete user?.name; // deletes user.name only if user exists

Summary

Optional chaining ?. comes in three forms:

obj?.prop→ obj.prop if obj exists, else undefined
obj?.[prop]→ obj[prop] if obj exists, else undefined
obj.method?.()→ calls obj.method() if it exists, else undefined
The three shapes of ?. and what each returns when the left side is missing.

Each one checks the left part for null or undefined and lets evaluation continue only when there’s something real to work with. Stringing them together lets you dig into nested data safely.

The discipline that matters: reach for ?. only where a missing value is a legitimate possibility in your logic. Used that way it removes clutter. Overused, it quietly hides bugs that a plain crash would have caught for you.