How to Use Optional Chaining (?.) in JavaScript
Working with real-world data in JavaScript means dealing with objects that may or may not have certain properties, APIs that may return null, and nested structures where any level could be missing. Before optional chaining existed, safely accessing deeply nested properties required verbose chains of manual checks that cluttered your code and obscured your intent.
Optional chaining (?.) is a modern JavaScript operator (introduced in ES2020) that lets you safely access nested properties, methods, and bracket-notation values without worrying about whether an intermediate value is null or undefined. If any part of the chain is missing, the expression short-circuits and returns undefined instead of throwing an error.
This guide covers the problem optional chaining solves, its three syntax forms, how short-circuiting works, and the critical question of when not to use it.
The Problem: Accessing Properties of null/undefined
Consider a common scenario: you receive user data from an API, and you need to access a deeply nested property.
let user = {
name: "Alice",
address: {
street: "123 Main St",
city: "Rome",
coordinates: {
lat: 41.9028,
lng: 12.4964
}
}
};
console.log(user.address.coordinates.lat); // 41.9028 (works fine)
This works perfectly when the data is complete. But what happens when a user has no address?
let user = {
name: "Bob"
// No address property
};
console.log(user.address.coordinates.lat);
// TypeError: Cannot read properties of undefined (reading 'coordinates')
user.address is undefined. Trying to read .coordinates on undefined throws a TypeError. This is one of the most common runtime errors in JavaScript applications.
The Pre-Optional-Chaining Solution
Before optional chaining, you had to manually check every step of the chain:
let user = { name: "Bob" };
// Approach 1: Multiple if checks
let lat;
if (user.address) {
if (user.address.coordinates) {
lat = user.address.coordinates.lat;
}
}
console.log(lat); // undefined (no error, but verbose)
// Approach 2: Logical AND short-circuit
let lat2 = user.address && user.address.coordinates && user.address.coordinates.lat;
console.log(lat2); // undefined (works, but hard to read)
// Approach 3: Ternary chains
let lat3 = user.address
? (user.address.coordinates ? user.address.coordinates.lat : undefined)
: undefined;
console.log(lat3); // undefined (even worse readability)
All three approaches work, but they are verbose, repetitive, and become increasingly unreadable as the nesting depth grows. Imagine doing this for a path like response.data.results[0].metadata.tags.primary. The manual checks would be painful.
The Optional Chaining Solution
Optional chaining replaces all of that with a clean, readable syntax:
let user = { name: "Bob" };
let lat = user.address?.coordinates?.lat;
console.log(lat); // undefined (no error, clean syntax)
One line. No error. The intent is immediately clear: "get the latitude if the address and coordinates exist."
Optional Chaining for Properties (?.)
The ?. operator checks whether the value before it is null or undefined. If it is, the evaluation stops immediately and returns undefined. If it is not, the access continues normally.
Basic Syntax
let result = obj?.property;
This is equivalent to:
let result = (obj !== null && obj !== undefined) ? obj.property : undefined;
Practical Examples
let user = {
name: "Alice",
profile: {
bio: "JavaScript developer",
social: {
twitter: "@alice_dev"
}
}
};
// All properties exist (works normally)
console.log(user.profile?.bio); // "JavaScript developer"
console.log(user.profile?.social?.twitter); // "@alice_dev"
// Missing properties (returns undefined instead of throwing)
console.log(user.profile?.social?.github); // undefined (github doesn't exist)
console.log(user.settings?.theme); // undefined (settings doesn't exist)
console.log(user.settings?.theme?.primary); // undefined (entire chain short-circuits)
Working with API Responses
Optional chaining is especially valuable when handling data from external sources where the structure is not guaranteed:
// Simulated API response (sometimes fields are missing)
let response = {
status: 200,
data: {
user: {
name: "Alice",
// company might not exist for all users
}
}
};
// Safe access without optional chaining would require:
// response && response.data && response.data.user && response.data.user.company && response.data.user.company.name
// With optional chaining:
let companyName = response.data?.user?.company?.name;
console.log(companyName); // undefined (no error)
// With a fallback value using ??
let displayCompany = response.data?.user?.company?.name ?? "No company listed";
console.log(displayCompany); // "No company listed"
Only null and undefined Trigger Short-Circuiting
An important detail: ?. only short-circuits for null and undefined. Other falsy values like 0, "" (empty string), and false are treated as valid values and do not trigger short-circuiting:
let data = {
count: 0,
label: "",
isActive: false
};
console.log(data.count?.toFixed(2)); // "0.00" (0 is NOT null/undefined)
console.log(data.label?.toUpperCase()); // "" (empty string is NOT null/undefined)
console.log(data.isActive?.toString()); // "false" (false is NOT null/undefined)
This is a key difference between ?. and the old && approach. The && operator short-circuits on any falsy value, which can cause problems:
let data = { count: 0 };
// ❌ Old approach with &&: breaks for falsy values
let result1 = data.count && data.count.toFixed(2);
console.log(result1); // 0 (short-circuited because 0 is falsy!)
// ✅ Optional chaining (works correctly)
let result2 = data.count?.toFixed(2);
console.log(result2); // "0.00" (0 is not null/undefined, so .toFixed runs)
Output:
0
0.00
Optional chaining (?.) checks for null or undefined only. It does not short-circuit on 0, "", false, or NaN. This makes it more precise and predictable than using && for safe property access.
Optional Chaining for Methods ?.()
Optional chaining is not limited to property access. You can also use it to safely call methods that might not exist, using the ?.() syntax.
Syntax
obj.method?.();
This checks if method exists and is callable. If obj.method is null or undefined, the call is skipped and the expression evaluates to undefined. If it exists, it is called normally.
let user = {
name: "Alice",
greet() {
return `Hello, I'm ${this.name}`;
}
// No farewell method
};
console.log(user.greet?.()); // "Hello, I'm Alice"
console.log(user.farewell?.()); // undefined (method doesn't exist, no error)
Output:
Hello, I'm Alice
undefined
Without optional chaining, calling user.farewell() would throw: TypeError: user.farewell is not a function.
Real-World Use Case: Feature Detection
Optional method chaining is particularly useful for feature detection, where a method might exist in some environments but not others:
// Some APIs might not be available in all browsers
let result = document.querySelector("#app")?.animate?.(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 300 }
);
// If #app doesn't exist or .animate is not supported, result is undefined
Handling Optional Callbacks
When a function accepts an optional callback parameter, ?.() provides a clean way to call it only if it was provided:
function fetchData(url, onSuccess, onError) {
fetch(url)
.then(response => response.json())
.then(data => {
onSuccess?.(data); // Call only if onSuccess was provided
})
.catch(error => {
onError?.(error); // Call only if onError was provided
console.error("Fetch failed:", error);
});
}
// Both callbacks are optional:
fetchData("https://api.example.com/data");
fetchData("https://api.example.com/data", (data) => console.log(data));
fetchData("https://api.example.com/data", null, (err) => console.error(err));
?.() Checks Existence, Not Type?.() checks whether the value is null or undefined before calling it. It does not verify that the value is actually a function. If the property exists but is not a function, you will still get a TypeError:
let user = { name: "Alice" };
user.name?.(); // TypeError: user.name is not a function
// user.name is "Alice" (a string), which is not null/undefined,
// so ?.() tries to call it, and fails
Optional chaining protects you from missing values, not from incorrect types.
Optional Chaining for Bracket Notation ?.[]
The third form of optional chaining works with bracket notation, which is used when property names are dynamic (stored in variables) or contain special characters.
Syntax
obj?.[expression]
This checks if obj is null or undefined. If it is, the expression returns undefined. If it is not, it accesses obj[expression] as normal.
let user = {
name: "Alice",
preferences: {
"dark-mode": true,
"font-size": 16
}
};
// Properties with special characters require bracket notation
console.log(user.preferences?.["dark-mode"]); // true
console.log(user.preferences?.["font-size"]); // 16
// When the object might not exist
console.log(user.settings?.["dark-mode"]); // undefined (no error)
Dynamic Property Names
let user = {
name: "Alice",
scores: {
math: 95,
science: 88,
history: 72
}
};
function getScore(user, subject) {
return user.scores?.[subject] ?? "No score available";
}
console.log(getScore(user, "math")); // 95
console.log(getScore(user, "art")); // "No score available"
console.log(getScore(null, "math")); // "No score available"
Output:
95
No score available
No score available
Accessing Array Elements Safely
Since arrays are objects, you can use ?.[] with numeric indices:
let users = [
{ name: "Alice" },
{ name: "Bob" }
];
console.log(users?.[0]?.name); // "Alice"
console.log(users?.[5]?.name); // undefined (index 5 doesn't exist)
console.log(null?.[0]?.name); // undefined (array itself is null)
// Useful for data that might be an array or might be null
function getFirstUserName(data) {
return data?.users?.[0]?.name ?? "Unknown";
}
console.log(getFirstUserName({ users: [{ name: "Alice" }] })); // "Alice"
console.log(getFirstUserName({ users: [] })); // "Unknown"
console.log(getFirstUserName({})); // "Unknown"
console.log(getFirstUserName(null)); // "Unknown"
Short-Circuiting Behavior
Optional chaining uses short-circuit evaluation. When the ?. encounters null or undefined on its left side, it immediately stops evaluating the entire rest of the expression and returns undefined.
The Whole Right Side Is Skipped
let user = null;
let result = user?.address.street.toUpperCase();
console.log(result); // undefined
Even though .address.street.toUpperCase() would normally cause multiple errors (accessing address on null, then street on undefined, etc.), none of this happens. The ?. after user sees that user is null, and the entire rest of the expression is abandoned. No further property access or method calls are attempted.
Side Effects Are Not Triggered
Because short-circuiting skips the rest of the expression, any side effects that would occur in the skipped part do not happen:
let callCount = 0;
function expensiveOperation() {
callCount++;
console.log("Expensive operation called!");
return { value: 42 };
}
let user = null;
let result = user?.data[expensiveOperation().value];
console.log(result); // undefined
console.log(callCount); // 0 (expensiveOperation was never called!)
Output:
undefined
0
The expensiveOperation() function was never called because user?. short-circuited when it found user is null. Everything to the right of ?. was skipped entirely.
Each ?. Only Protects Its Own Link
It is important to understand that each ?. operator independently checks the value immediately before it. It does not protect previous parts of the chain:
let data = {
users: null
};
// This will throw an error:
// data.users.list?.length
// Because data.users is null, and we used regular . to access .list
// TypeError: Cannot read properties of null (reading 'list')
You need ?. at each potentially nullable link:
let data = { users: null };
let length = data.users?.list?.length;
console.log(length); // undefined (no error)
Chaining Multiple Optional Accesses
let company = {
name: "TechCorp"
// departments is undefined
};
// Each ?. independently short-circuits if needed
let managerEmail = company
.departments
?.engineering
?.manager
?.email;
// Fails at company.departments because departments is undefined
// and we used a regular dot before ?.
// ✅ Correct: use ?. at every uncertain link
let managerEmail = company
?.departments
?.engineering
?.manager
?.email;
console.log(managerEmail); // undefined
?.Place ?. only before properties that might not exist. If you know a property exists (like company in the example above being a non-null object you just defined), use a regular dot . for that access. Use ?. at the point where uncertainty begins.
Combining Optional Chaining with Other Operators
Optional chaining works beautifully with the nullish coalescing operator (??) to provide default values:
let user = {
name: "Alice",
settings: {
// theme is missing
}
};
// Optional chaining + nullish coalescing = safe access with defaults
let theme = user.settings?.theme ?? "light";
console.log(theme); // "light"
let fontSize = user.settings?.fontSize ?? 16;
console.log(fontSize); // 16
let name = user.name ?? "Anonymous";
console.log(name); // "Alice" (name exists, so ?? doesn't trigger)
Why ?? Instead of ||
Using || for defaults has a subtle bug with falsy values. Using ?? with ?. is more precise:
let config = {
settings: {
volume: 0, // Valid setting: volume is muted
label: "", // Valid setting: no label
enabled: false // Valid setting: explicitly disabled
}
};
// ❌ WRONG: || treats 0, "", and false as "missing"
console.log(config.settings?.volume || 50); // 50 (wrong! 0 is a valid volume)
console.log(config.settings?.label || "N/A"); // "N/A" (wrong! "" is intentional)
console.log(config.settings?.enabled || true); // true (wrong! false is intentional)
// ✅ CORRECT: ?? only triggers for null/undefined
console.log(config.settings?.volume ?? 50); // 0 (correct!)
console.log(config.settings?.label ?? "N/A"); // "" (correct!)
console.log(config.settings?.enabled ?? true); // false (correct!)
Output for ||:
50
N/A
true
Output for ??:
0
false
Optional Chaining Cannot Be Used for Writing
Optional chaining is a read-only operation. You cannot use it on the left side of an assignment:
let user = null;
// ❌ SyntaxError: Invalid left-hand side in assignment
// user?.name = "Alice";
// ✅ You must check manually before writing
if (user) {
user.name = "Alice";
}
This is by design. Writing to a property of null or undefined makes no sense (there is no object to write to), so the language does not allow it.
When NOT to Overuse Optional Chaining
Optional chaining is powerful, but using it everywhere without thought can hide bugs and make your code harder to debug. Here are the cases where you should not use it.
Do Not Silence Errors That Should Be Loud
If a value should always exist and its absence indicates a bug in your code, do not use optional chaining to silently swallow the error. Let the error happen so you can find and fix the root cause.
// ❌ BAD: Hiding a programming error
function processOrder(order) {
// order should ALWAYS have items (it's a required field)
let firstItem = order?.items?.[0]?.name;
// If items is missing, this silently returns undefined
// instead of alerting you to a bug in order creation
console.log(`Processing: ${firstItem}`);
}
processOrder({}); // "Processing: undefined" (silent failure, bug hidden)
// ✅ GOOD: Fail loudly when data should exist
function processOrder(order) {
if (!order.items || order.items.length === 0) {
throw new Error("Order must have at least one item");
}
let firstItem = order.items[0].name;
console.log(`Processing: ${firstItem}`);
}
processOrder({}); // Error: Order must have at least one item
Do Not Use ?. on Values You Know Exist
If a variable is guaranteed to be defined (you just created it, or it is a required parameter validated at the function entry), adding ?. is unnecessary noise:
// ❌ UNNECESSARY: user was just created, it's definitely not null
let user = { name: "Alice", age: 30 };
console.log(user?.name); // Works, but ?. is pointless here
console.log(user?.age); // Same (user definitely exists)
// ✅ CLEAN: Use regular dot when you know the value exists
console.log(user.name); // "Alice"
console.log(user.age); // 30
Do Not Chain ?. Excessively Deep
If you find yourself writing extremely long optional chains, it may indicate a structural problem in your data model:
// ❌ Too many optional accesses: is this data structure reliable at all?
let value = response?.data?.results?.[0]?.metadata?.tags?.primary?.label?.text;
If nearly every level of your data could be missing, consider:
- Normalizing or validating the data at the entry point
- Using a schema validation library (Zod, Yup, Joi) to ensure data conforms to expected shapes
- Providing default structures
// ✅ BETTER: Validate and normalize data at the boundary
function normalizeResponse(response) {
return {
results: response?.data?.results ?? [],
// ... normalize other fields
};
}
let normalized = normalizeResponse(rawResponse);
// Now you can access properties with confidence
let firstResult = normalized.results[0]; // Might be undefined (empty array), but predictable
Do Not Mask null Where It Has Meaning
Sometimes, the difference between null and undefined is meaningful. Using ?. everywhere collapses both into undefined, potentially losing important information:
let user = {
name: "Alice",
middleName: null // Explicitly set to null (user has no middle name)
// lastName is not set at all (it's undefined)
};
// Both return undefined with ?.
console.log(user.middleName?.toUpperCase()); // undefined
console.log(user.lastName?.toUpperCase()); // undefined
// But the distinction matters:
// middleName is null (we know the user has no middle name)
// lastName is undefined (we might have forgotten to include it (a bug?))
Use ?. where something legitimately might not exist (optional configuration, API data with optional fields, user input that may be incomplete). Do not use ?. to suppress errors for data that should exist. Errors are information. Silent failures are bugs waiting to happen.
Quick Decision Guide
Ask yourself before adding ?.:
"Is it acceptable and expected for this value to be null or undefined at this point?"
- Yes (optional field, external data, user might not have filled it in): Use
?. - No (required field, value you just created, core application state): Use
.and let errors surface
Summary
- Optional chaining (
?.) safely accesses properties of values that might benullorundefined, returningundefinedinstead of throwing aTypeError. - Three syntax forms exist: property access (
obj?.prop), method calls (obj.method?.()), and bracket notation (obj?.["prop"]orobj?.[index]). ?.checks only fornullandundefined. Other falsy values (0,"",false) are treated as valid and do not trigger short-circuiting. This makes it more precise than the old&&approach.- Short-circuiting stops the entire evaluation chain at the first
null/undefined. No further property access, method calls, or side effects occur. - Combine
?.with the nullish coalescing operator (??) to provide clean default values:user.settings?.theme ?? "light". - Optional chaining is read-only. You cannot use it on the left side of an assignment.
- Do not overuse optional chaining. If a value should always exist, let the error surface. Use
?.only wherenull/undefinedis a legitimate, expected possibility.
Optional chaining, paired with nullish coalescing, represents one of the most practical improvements to JavaScript in recent years. It eliminates an entire class of verbose defensive coding while keeping your intent crystal clear.