Advanced JS Optional Chaining Complete Guide
An advanced guide to JavaScript optional chaining. Covers the ?. operator for property access, method calls, and bracket notation, short-circuit evaluation, combining with nullish coalescing, optional chaining in arrays, deeply nested API responses, and performance considerations.
Optional chaining (?.) short-circuits a property access chain to undefined when any intermediate value is null or undefined. Introduced in ES2020, it eliminates verbose null-checking and makes deeply nested data access safe and readable.
The Problem Optional Chaining Solves
Without optional chaining, accessing nested properties requires defensive checks:
// Without optional chaining
const city = user && user.address && user.address.city;
// With optional chaining
const city = user?.address?.city;Both produce undefined if anything in the chain is nullish, but the optional chaining version is shorter and clearer.
Three Forms of Optional Chaining
| Form | Syntax | What It Guards |
|---|---|---|
| Property access | obj?.prop | obj being null/undefined |
| Bracket notation | obj?.[expr] | obj being null/undefined |
| Method call | obj?.method() | obj.method being null/undefined |
Property Access
const user = { profile: { name: "Alice" } };
console.log(user?.profile?.name); // "Alice"
console.log(user?.address?.city); // undefined (no error)
console.log(null?.profile?.name); // undefinedBracket Notation
const data = { "user-name": "Alice", items: ["a", "b", "c"] };
console.log(data?.["user-name"]); // "Alice"
console.log(data?.["missing"]); // undefined
// With dynamic keys
const key = "items";
console.log(data?.[key]?.[0]); // "a"Method Calls
const logger = {
log: (msg) => console.log(msg),
};
logger?.log?.("hello"); // "hello"
logger?.warn?.("caution"); // undefined (warn does not exist, no error)The method call form checks if the method exists before invoking it.
Short-Circuit Evaluation
When ?. encounters null or undefined, the entire rest of the chain is skipped:
const result = null?.a.b.c.d(); // undefined — b, c, d() are never evaluatedNo property access or function call after the ?. runs if the left side is nullish.
Side Effects Are Skipped
let called = false;
const obj = null;
obj?.method(called = true);
console.log(called); // false — the argument expression was never evaluatedCombining With Nullish Coalescing
Optional chaining returns undefined when short-circuited. Combine with ?? to provide a default:
const theme = user?.settings?.theme ?? "light";
// If user, settings, or theme is missing: "light"
// If theme is "" (empty string) or 0: keeps the value (not nullish)| Expression | user = null | user.settings.theme = "dark" | user.settings.theme = "" |
|---|---|---|---|
user?.settings?.theme ?? "light" | "light" | "dark" | "" |
user?.settings?.theme || "light" | "light" | "dark" | "light" |
The ?? operator only falls back for null/undefined, while || also treats "", 0, and false as falsy. See advanced JS nullish coalescing full tutorial for a deep dive.
Deeply Nested API Responses
Real-world APIs often return optional nested data:
async function getUserCity(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Safe access through multiple optional levels
return data?.user?.addresses?.[0]?.city ?? "Unknown";
}Normalizing API Data
function normalizeUser(apiResponse) {
return {
id: apiResponse?.id ?? null,
name: apiResponse?.profile?.displayName ?? "Anonymous",
email: apiResponse?.contact?.email ?? null,
avatar: apiResponse?.profile?.images?.[0]?.url ?? "/default-avatar.png",
role: apiResponse?.permissions?.role ?? "viewer",
joinedAt: apiResponse?.metadata?.createdAt ?? null,
};
}Optional Chaining in Loops and Callbacks
const users = [
{ name: "Alice", address: { city: "Portland" } },
{ name: "Bob" },
null,
{ name: "Carol", address: { city: "Seattle" } },
];
const cities = users
.filter(Boolean) // remove null entries
.map(user => user?.address?.city ?? "No city")
.filter(city => city !== "No city");
console.log(cities); // ["Portland", "Seattle"]Optional Chaining With delete
const config = { debug: { verbose: true } };
delete config?.debug?.verbose; // Works — deletes the property
delete config?.missing?.prop; // No error — short-circuitsOptional Chaining Does Not Guard Assignment
const obj = null;
// This DOES throw — you cannot assign through optional chaining
// obj?.name = "Alice"; // SyntaxError
// To conditionally assign, use an if check
if (obj != null) {
obj.name = "Alice";
}Optional chaining is read-only. It works for access and delete, not for assignment.
Common Patterns
Config With Multiple Fallbacks
function getApiUrl(config) {
return (
config?.api?.url ??
config?.api?.baseUrl ??
process.env?.API_URL ??
"http://localhost:3000"
);
}Safe DOM Access
const width = document.querySelector(".sidebar")?.getBoundingClientRect()?.width ?? 0;Optional Callback Execution
function processData(data, options) {
const transformed = transform(data);
options?.onSuccess?.(transformed); // call only if provided
return transformed;
}
// Both are valid
processData(data, { onSuccess: result => console.log(result) });
processData(data, {}); // onSuccess not called, no errorRune AI
Key Insights
- Three forms exist:
obj?.prop,obj?.[expr], andobj?.method()-- each guards the left-hand side against being null or undefined - Short-circuit skips the entire rest of the chain: Once a nullish value is encountered, nothing further in that chain evaluates
- Combine with ?? for defaults:
user?.name ?? "Anonymous"provides a fallback only for null/undefined, not for falsy values like "" or 0 - Cannot be used for assignment:
obj?.prop = valueis a SyntaxError; use a conditionalifcheck for assignment - Replaces verbose && chains:
user?.address?.cityis equivalent touser && user.address && user.address.citybut more precise and readable
Frequently Asked Questions
Does optional chaining work with 0 and empty string?
Can I use optional chaining on the left side of an assignment?
Is there a performance cost?
Does optional chaining work with private class fields?
Is ?. the same as the old && guard pattern?
Conclusion
Optional chaining eliminates defensive null-checking boilerplate for nested property access, bracket notation, and method calls. Pair it with nullish coalescing (??) for clean fallback defaults. Together, these ES2020 operators handle the vast majority of null-safety patterns. For the nullish coalescing companion, see advanced JS nullish coalescing full tutorial. For the related logical assignment operators, see logical assignment operators in JS complete guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.