3.10 Optional Chaining Operator (?.)
Overview
The optional chaining operator (?.) allows you to safely access deeply nested object properties without having to explicitly check if each reference in the chain is valid. If any reference is null or undefined, the expression short-circuits and returns undefined instead of throwing an error.
Table of Contents
- •The Problem It Solves
- •Basic Syntax
- •Types of Optional Chaining
- •How It Works
- •Comparison with Traditional Approaches
- •Common Use Cases
- •Best Practices
- •Common Mistakes
- •Browser Support
The Problem It Solves
Without Optional Chaining
const user = {
name: 'John',
address: null,
};
console.log(user.address.city);
if (user && user.address && user.address.city) {
console.log(user.address.city);
}
const city = user && user.address && user.address.city;
With Optional Chaining
const city = user?.address?.city;
Basic Syntax
object?.property
object?.[expression]
object?.method()
array?.[index]
| Syntax | Description | Returns |
|---|
obj?.prop | Access property if obj exists | Value or undefined |
obj?.[expr] | Access computed property | Value or undefined |
obj?.method() | Call method if it exists | Result or undefined |
arr?.[index] | Access array element | Value or undefined |
Types of Optional Chaining
1. Property Access (?.)
const user = {
profile: {
name: 'Alice',
avatar: {
url: 'image.png',
},
},
};
console.log(user?.profile?.name);
console.log(user?.profile?.avatar?.url);
console.log(user?.settings?.theme);
console.log(user?.profile?.bio?.text);
2. Bracket Notation (?.[])
const data = {
users: ['Alice', 'Bob', 'Charlie'],
};
const prop = 'users';
console.log(data?.[prop]);
console.log(data?.['users']);
console.log(data?.['admins']);
console.log(data?.users?.[0]);
console.log(data?.users?.[10]);
console.log(data?.admins?.[0]);
3. Method Calls (?.())
const calculator = {
add: (a, b) => a + b,
};
console.log(calculator.add?.(5, 3));
console.log(calculator.subtract?.(5, 3));
function processData(callback) {
callback?.();
}
processData(() => console.log('Called!'));
processData(null);
How It Works
Short-Circuit Evaluation
obj?.prop?.method?.()
│ │ │
▼ ▼ ▼
Check Check Check
obj prop method
│ │ │
▼ ▼ ▼
null/ null/ not a
undef? undef? function?
│ │ │
▼ ▼ ▼
Return Return Return
undef undef undefined
Evaluation Flow
const result = a?.b?.c?.d;
const result =
a === null || a === undefined
? undefined
: a.b === null || a.b === undefined
? undefined
: a.b.c === null || a.b.c === undefined
? undefined
: a.b.c.d;
Important: Only Checks for null and undefined
const obj = {
zero: 0,
empty: '',
falsy: false,
};
console.log(obj?.zero);
console.log(obj?.empty);
console.log(obj?.falsy);
const obj2 = {
nullVal: null,
undefVal: undefined,
};
console.log(obj2?.nullVal?.prop);
console.log(obj2?.undefVal?.prop);
Comparison with Traditional Approaches
| Approach | Syntax | Behavior on null/undefined |
|---|
| Direct access | obj.prop | Throws TypeError |
| Logical AND | obj && obj.prop | Returns falsy value or prop |
| Ternary | obj ? obj.prop : undefined | Returns undefined or prop |
| Optional chaining | obj?.prop | Returns undefined |
Detailed Comparison
const user = null;
const name1 = user && user.name;
const name2 = user != null ? user.name : undefined;
const name3 = user?.name;
Optional Chaining vs Logical AND
const data = { value: 0 };
console.log(data && data.value && data.value.toString());
console.log(data?.value?.toString?.());
Common Use Cases
1. API Response Handling
const response = await fetch('/api/user');
const data = await response.json();
const userName = data?.user?.profile?.name ?? 'Guest';
const avatarUrl = data?.user?.profile?.avatar?.url ?? '/default-avatar.png';
const permissions = data?.user?.roles?.[0]?.permissions ?? [];
2. Configuration Objects
function initApp(config) {
const theme = config?.ui?.theme ?? 'light';
const fontSize = config?.ui?.fontSize ?? 14;
const enableLogging = config?.debug?.logging ?? false;
config?.onInit?.();
return { theme, fontSize, enableLogging };
}
initApp({ ui: { theme: 'dark' } });
initApp();
initApp(null);
3. DOM Element Access
const value = document.querySelector('#myInput')?.value ?? '';
const text = document.querySelector('.content')?.textContent?.trim() ?? '';
document.addEventListener('click', (e) => {
const buttonId = e.target?.closest?.('button')?.id;
if (buttonId) {
handleButtonClick(buttonId);
}
});
4. Array Operations
const data = {
users: [
{ name: 'Alice', scores: [95, 87, 92] },
{ name: 'Bob' },
],
};
const firstUserFirstScore = data?.users?.[0]?.scores?.[0];
const secondUserFirstScore = data?.users?.[1]?.scores?.[0];
const averageScore =
data?.users?.[0]?.scores?.reduce?.((sum, score) => sum + score, 0) /
(data?.users?.[0]?.scores?.length ?? 1);
5. Event Handlers and Callbacks
class EventEmitter {
constructor() {
this.handlers = {};
}
on(event, handler) {
this.handlers[event] = handler;
}
emit(event, data) {
this.handlers[event]?.(data);
}
}
const emitter = new EventEmitter();
emitter.emit('click', { x: 100 });
Best Practices
✅ Do: Use with Potentially Null Data
const userName = apiResponse?.user?.name;
const theme = userPrefs?.display?.theme;
✅ Do: Combine with Nullish Coalescing
const port = config?.server?.port ?? 3000;
const timeout = options?.timeout ?? 5000;
✅ Do: Use for Optional Callbacks
function fetchData(url, options) {
options?.onStart?.();
return fetch(url)
.then((res) => {
options?.onSuccess?.(res);
return res;
})
.catch((err) => {
options?.onError?.(err);
throw err;
});
}
❌ Don't: Overuse on Known Structures
const user = { name: 'Alice', age: 25 };
const name = user?.name;
const name = user.name;
❌ Don't: Use to Hide Bugs
function processUser(user) {
console.log(user?.name);
}
function processUser(user) {
if (!user?.name) {
throw new Error('User must have a name');
}
console.log(user.name);
}
❌ Don't: Chain Unnecessarily
const x = obj?.a?.b?.c?.d?.e?.f?.g;
if (!obj?.a?.b) {
throw new Error('Invalid object structure');
}
const x = obj.a.b.c.d.e.f.g;
Common Mistakes
1. Using with Assignment (Left-hand Side)
user?.name = "Alice";
if (user) {
user.name = "Alice";
}
user ??= {};
user.name = "Alice";
2. Confusing with Logical AND
const obj = { count: 0 };
console.log(obj && obj.count);
console.log(obj?.count);
const obj2 = { empty: '' };
console.log(obj2 && obj2.empty && obj2.empty.length);
console.log(obj2?.empty?.length);
3. Forgetting It Returns undefined
const result = null?.property;
console.log(result);
console.log(result === null);
console.log(result === undefined);
if (obj?.value) {
}
4. Incorrect Syntax with Function Calls
const obj = {
method: () => 'result',
};
obj.method?.();
obj?.method?.();
obj?.method();
Browser Support
| Browser | Version |
|---|
| Chrome | 80+ |
| Firefox | 74+ |
| Safari | 13.1+ |
| Edge | 80+ |
| Node.js | 14.0+ |
Transpilation
For older environments, Babel can transpile optional chaining:
npm install @babel/plugin-proposal-optional-chaining
Summary
| Feature | Syntax | Returns |
|---|
| Property access | obj?.prop | Value or undefined |
| Bracket notation | obj?.[key] | Value or undefined |
| Method call | obj?.method() | Result or undefined |
| Combined | obj?.a?.b?.() | Final value or undefined |
Key Points
- •Only checks for
null and undefined - other falsy values don't trigger short-circuiting
- •Returns
undefined when short-circuited - not null
- •Cannot be used for assignment - only for reading values
- •Combine with
?? for default values
- •Don't overuse - it can hide legitimate bugs
Next Steps
- •Practice with the examples in
examples.js
- •Complete the exercises in
exercises.js
- •Learn how it combines with Nullish Coalescing (
??) for powerful patterns