JavaScript Optional Chaining (?.)

Optional chaining (?.) is an operator that lets you read the value of a property deep inside a chain of connected objects without having to manually check that each link in the chain exists. Instead of writing a pile of if statements to guard against null or undefined, you insert a ?. right before the part of the expression that might not exist, and JavaScript stops evaluating and returns undefined the moment it hits a missing value. It became part of the language in ES2020 and is now one of the most commonly used features in modern JavaScript, especially when working with API responses, configuration objects, and DOM elements that may or may not be present.

Overview / How it works

Before optional chaining existed, accessing a deeply nested property safely meant writing something like user && user.address && user.address.city, or wrapping the access in a try/catch. Both approaches are verbose and easy to get wrong, especially as the chain gets longer. Optional chaining collapses that entire guard clause into a single operator. When JavaScript evaluates a?.b, it first checks whether a is null or undefined. If it is, the whole expression immediately evaluates to undefined and nothing to the right of the ?. is evaluated at all — not even function calls. If a is anything else (including falsy values like 0, "", or false), evaluation proceeds normally and a.b is read.

This distinction matters a lot: optional chaining only checks for nullish values (null and undefined), not falsy values in general. A property holding 0 or an empty string is still considered “present” and the chain continues past it. This is what makes optional chaining pair so naturally with the nullish coalescing operator ??, which lets you supply a fallback only when the left side is actually null or undefined.

Optional chaining works with three different kinds of access: plain property access (obj?.prop), computed/bracket access (obj?.[key]), and function calls (obj.method?.()). Each of these can be freely combined and chained together, and once a ?. in the middle of a chain short-circuits, the entire rest of the expression to its right is skipped — the engine never even attempts to evaluate it, which is why it’s safe to chain calls and lookups after an optional link.

Syntax

obj?.prop;
obj?.[expression];
obj?.method?.(args);
a?.b?.c?.d;
Form Meaning
obj?.prop Reads prop only if obj is not null/undefined; otherwise returns undefined.
obj?.[expression] Computed access — useful when the key is a variable or comes from user input.
obj.method?.() Calls method only if it exists and is callable; otherwise returns undefined instead of throwing.
a?.b?.c?.d Each link is checked independently; the first nullish value stops the whole chain.

Examples

Example 1: Safe nested property access

const user = {
  name: "Ava",
  address: {
    city: "Lisbon",
    geo: { lat: 38.7, lng: -9.1 }
  }
};

console.log(user.address?.city);
console.log(user.address?.zip);
console.log(user.contact?.email);
console.log(user.contact?.geo?.lat);
Output:
Lisbon
undefined
undefined
undefined

The first call succeeds because user.address exists, so city is read normally. The second call returns undefined simply because zip was never set — that part is ordinary property access, not optional chaining doing anything special. The third and fourth calls are where ?. earns its keep: user.contact does not exist at all, so without optional chaining, user.contact.email would throw TypeError: Cannot read properties of undefined (reading 'email'). With ?., the moment JavaScript sees that user.contact is undefined, it stops and returns undefined instead of crashing — and this holds even for the deeper .geo.lat chain in the last line.

Example 2: Optional method calls

const api = {
  fetchUser(id) {
    return { id, name: "Rui" };
  }
};

const backupApi = {};

console.log(api.fetchUser?.(1));
console.log(backupApi.fetchUser?.(1));
console.log(backupApi.fetchUser?.(1) ?? "No API available");
Output:
{ id: 1, name: 'Rui' }
undefined
No API available

This pattern is extremely common when working with optional callbacks, plugin hooks, or objects that implement only part of an interface. api.fetchUser?.(1) calls the method normally because it exists. backupApi.fetchUser?.(1) checks whether fetchUser is nullish before trying to call it — since backupApi has no such method, the whole expression short-circuits to undefined instead of throwing TypeError: backupApi.fetchUser is not a function. Combining this with ?? on the last line lets you supply a clean default in a single expression.

Example 3: Computed access and real-world parsing

function getSetting(config, section, key) {
  return config?.[section]?.[key];
}

const config = {
  theme: { color: "dark", fontSize: 14 }
};

console.log(getSetting(config, "theme", "color"));
console.log(getSetting(config, "theme", "language"));
console.log(getSetting(config, "layout", "color"));
console.log(getSetting(null, "theme", "color"));

const users = [{ name: "Mia" }, null, { name: "Leo" }];
for (const user of users) {
  console.log(user?.name ?? "Unknown");
}
Output:
dark
undefined
undefined
undefined
Mia
Unknown
Leo

config?.[section]?.[key] shows optional chaining with bracket notation, which is essential when the property name is stored in a variable rather than known ahead of time. Passing "layout" (a section that doesn’t exist) or passing null as the whole config object both resolve safely to undefined because the chain stops at the first missing link. The loop at the end shows a very common real-world use: iterating over an array that might contain null entries (for example, deleted users in a list) and reading a property off each one without special-casing the null values.

Under the hood

When the JavaScript engine parses an expression containing ?., it doesn’t treat it as an ordinary operator applied to a fully-evaluated left side. Instead, it builds what the spec calls an “optional chain”: a sequence of operations where, as soon as one link is marked optional and its base value turns out to be null or undefined, the engine short-circuits the entire remaining chain and produces undefined — it does not just skip that one access and keep going with the rest.

Concretely, for a?.b.c.d, if a is null, the engine never attempts to read .c or .d either, even though those parts don’t have their own ?.. This is different from writing separate guards for each property, and it’s why a single ?. placed at the right spot is enough to protect an entire tail of property or method accesses. Function arguments in an optional call, like obj.method?.(compute()), are also never evaluated if the call short-circuits — compute() simply never runs when obj.method is nullish.

Common Mistakes

Mistake 1: Trying to assign through optional chaining. Optional chaining produces a value, not a reference you can assign to. Writing user.address?.city = "Porto"; is not allowed — a chain that could short-circuit to undefined can’t also be a valid assignment target, so JavaScript throws a SyntaxError at parse time. If you need to conditionally assign, check for the object’s existence first (or create it) rather than trying to combine the check and the write in one expression.

// Invalid — SyntaxError: Invalid left-hand side in assignment
user.address?.city = "Porto";

Mistake 2: Letting optional chaining silently hide typos and real bugs. Because ?. swallows any missing property into a quiet undefined, a simple typo in a property name can go unnoticed for a long time instead of throwing a loud, easy-to-spot error.

const response = { data: { items: [1, 2, 3] } };

// Typo: "itmes" instead of "items" — optional chaining hides the bug
console.log(response.data?.itmes?.length);
Output:
undefined

Here the intent was clearly to read response.data.items.length, but the typo means the code silently returns undefined instead of failing loudly. Optional chaining should be reserved for values that are genuinely optional (a field that may legitimately be missing), not sprinkled everywhere as a way to avoid thinking about whether a property should exist.

Best Practices

  • Use ?. only where a value is genuinely optional (an API field that may be absent, a callback that may not be provided) — not as a blanket habit that masks real bugs.
  • Pair ?. with ?? to provide a default value in one expression, e.g. const city = user.address?.city ?? "Unknown";.
  • Remember ?. checks for null/undefined only — don’t rely on it to filter out other falsy values like 0, "", or false.
  • Use obj?.[expr] for computed keys instead of falling back to manual && chains.
  • Prefer obj.method?.() over checking typeof obj.method === "function" when calling optional callbacks or interface methods.
  • Don’t overuse optional chaining on objects your own code fully controls and guarantees the shape of — plain property access is clearer and lets real bugs surface immediately.

Practice Exercises

  • Given const company = { name: "Acme", ceo: { name: "Sam" } };, write a single expression using ?. and ?? that logs the CFO’s name if it exists, or "No CFO" if company.cfo is missing.
  • Write a function callIfExists(obj, methodName, ...args) that uses ?.[methodName]?.() to call a method on obj only if it exists, returning undefined otherwise. Test it on an object that has the method and one that doesn’t.
  • Given an array of order objects where some orders may be null and some may be missing a shipping field (for example [{ shipping: { city: "Rome" } }, null, {}]), use optional chaining inside a loop to print each order’s shipping city or "No shipping info".

Summary

  • Optional chaining (?.) safely reads a property, calls a method, or indexes a value only if the base is not null/undefined, otherwise it short-circuits to undefined.
  • It comes in three forms: obj?.prop, obj?.[expr], and obj.method?.().
  • A short-circuit skips the entire rest of the chain to the right, including function arguments that would otherwise be evaluated.
  • It checks for nullish values only, not general falsiness — 0, "", and false do not trigger the short-circuit.
  • You cannot assign to an optional chain (obj?.prop = value is a SyntaxError).
  • Combine ?. with ?? for clean, one-line default values.
  • Use it deliberately for truly optional data — overusing it can hide typos and real bugs behind silent undefined results.