Accessing deeply nested properties in JavaScript used to require defensive checks at every level. One missing piece of data and your entire application crashes with "Cannot read property of undefined." If you have ever debugged that error in AI-generated code, you already know the pain. Optional chainingWhat is optional chaining?The ?. operator that safely accesses nested properties by returning undefined instead of crashing when a value along the chain is null or undefined. and nullish coalescingWhat is nullish coalescing?The ?? operator that provides a fallback value only when the left side is null or undefined, unlike || which also replaces 0, empty strings, and false. give you concise syntax for handling missing data safely.
The problem with deep property access
Imagine you are building a user profile page. The APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. returns user data, but some fields might be missing.
const user = {
name: 'Alice',
address: { city: 'Paris', country: 'France' }
};
console.log(user.address.city); // 'Paris' - works fine
console.log(user.phone.number); // TypeError! user.phone is undefinedBefore optional chainingWhat is optional chaining?The ?. operator that safely accesses nested properties by returning undefined instead of crashing when a value along the chain is null or undefined., you would write verbose guard clauses:
const city = user && user.address && user.address.city;That is more null-checking code than actual business logic.
The optional chainingWhat is optional chaining?The ?. operator that safely accesses nested properties by returning undefined instead of crashing when a value along the chain is null or undefined. operator (?.)
The ?. operator works like the dot operator with a safety net. If the value before ?. is null or undefined, the expression short-circuits and returns undefined instead of throwing.
console.log(user?.address?.city); // 'Paris'
console.log(user?.phone?.number); // undefined (no crash)data?.items?.map(...) but data.items should always be an array, the real fix is to ensure the data source returns a proper array, not to sprinkle ?. everywhere. Optional chaining hides bugs when used to paper over broken data contracts. Treat it as a safety net for genuinely uncertain data, not a crutch for avoiding proper validation.Where optional chaining works
| Syntax | What it does | Example |
|---|---|---|
obj?.prop | Safe property access | user?.address?.city |
obj?.method() | Safe method call | callback?.() |
obj?.[expr] | Safe bracket access | arr?.[0] |
| Chained | Multiple levels | a?.b?.c?.d |
const user = {
getFullName: () => 'Alice Johnson'
};
console.log(user.getFullName?.()); // 'Alice Johnson'
console.log(user.getAge?.()); // undefined - no crashOptional chaining with arrays
const users = [
{ name: 'Alice', posts: [{ title: 'Hello' }] },
{ name: 'Bob' }
];
console.log(users[0]?.posts?.[0]?.title); // 'Hello'
console.log(users[1]?.posts?.[0]?.title); // undefined
console.log(users[5]?.name); // undefinedThe nullish coalescingWhat is nullish coalescing?The ?? operator that provides a fallback value only when the left side is null or undefined, unlike || which also replaces 0, empty strings, and false. operator (??)
Optional chainingWhat is optional chaining?The ?. operator that safely accesses nested properties by returning undefined instead of crashing when a value along the chain is null or undefined. solves "is it safe to access?" while ?? solves "what default should I use?"
The ?? operator returns the right-hand operand only when the left is null or undefined. This is different from ||, which treats all falsy values as "not valid."
// || treats 0, '', and false as falsy - replaces them
const count1 = 0 || 10; // 10 (0 was replaced!)
const name1 = '' || 'Guest'; // 'Guest'
// ?? only replaces null and undefined
const count2 = 0 ?? 10; // 0 (0 is a valid number)
const name2 = '' ?? 'Guest'; // '' (empty string is valid)This distinction matters more than you might expect. Imagine a settings object where 0 is a valid timeout and false means "disable this feature." Using || would replace those valid values with defaults.
| Operator | Replaces | Keeps |
|---|---|---|
\|\| | null, undefined, 0, '', false | truthy values only |
?? | null, undefined only | 0, '', false |
Combining both operators
This is the pattern you will use constantly, safe access with a meaningful default:
const config = {
database: { host: 'localhost' },
timeout: 0
};
const port = config?.database?.port ?? 5432;
// database exists but port is undefined -> uses 5432
const timeout = config?.timeout ?? 3000;
// timeout is 0, a valid number -> uses 0, NOT 3000
const apiUrl = config?.api?.url ?? 'https://api.default.com';
// api is undefined -> uses the default URLReal-world examples
Form handling
function submitForm(data) {
const email = data?.user?.email ?? '';
const age = data?.user?.age ?? 0;
if (!email) {
showError('Email is required');
return;
}
saveUser({ email, age });
}APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. response handling
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return {
name: data?.user?.name ?? 'Unknown User',
avatar: data?.user?.avatar?.url ?? '/default-avatar.png',
role: data?.user?.role ?? 'member'
};
}Configuration with defaults
function initializeApp(options) {
return {
apiUrl: options?.apiUrl ?? 'https://api.default.com',
timeout: options?.timeout ?? 5000,
retries: options?.retries ?? 3,
debug: options?.debug ?? false
};
}Quick reference
| Operator | Use when | Example |
|---|---|---|
?. | Accessing potentially missing properties | user?.profile?.bio |
?? | Providing defaults (keeps 0, false, '') | value ?? 'default' |
\|\| | Providing defaults (rejects all falsy) | value \|\| 'fallback' |
?.() | Calling optional methods | callback?.() |
?.[n] | Accessing optional array elements | arr?.[0] |
?. + ?? | Safe access with default | obj?.x ?? 'default' |