Safely access deeply nested properties with optional chaining (?.) and provide defaults with nullish coalescing (??).
Accessing nested properties on null or undefined throws a TypeError — one of the most common errors in JavaScript.
const user = { name: 'Alice' };
console.log(user.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')Traditional solution — check every level:
if (user && user.address && user.address.city) {
console.log(user.address.city);
}
// Or with ternary
const city = user && user.address ? user.address.city : 'Unknown';This is verbose and gets worse with deeper nesting:
if (user && user.company && user.company.address && user.company.address.zip) {
// finally safe to use user.company.address.zip
}Modern JavaScript provides two elegant operators to solve this problem: optional chaining (?.) and nullish coalescing (??).
The optional chaining operator (?.) returns undefined instead of throwing an error when accessing a property on null or undefined.
Property access:
const user = { name: 'Alice' };
console.log(user.address?.city); // undefined (no error!)
console.log(user.address?.city?.length); // undefinedBracket notation:
const key = 'city';
console.log(user.address?.[key]); // undefinedMethod calls:
const user = { name: 'Alice' };
console.log(user.greet?.()); // undefined (no error even though greet doesn't exist)
const admin = {
name: 'Bob',
greet() { return `Hi, I'm ${this.name}`; }
};
console.log(admin.greet?.()); // "Hi, I'm Bob"Short-circuit behavior: When the left side is null or undefined, the entire chain stops and returns undefined. Nothing to the right is evaluated:
const user = null;
console.log(user?.address?.city); // undefined (no error)
// user is null, so .address is never accessedNote: Optional chaining only checks for null and undefined, not other falsy values like 0, '', or false.
The nullish coalescing operator (??) provides a default value when the left side is null or undefined.
const name = null ?? 'Anonymous';
console.log(name); // 'Anonymous'
const score = undefined ?? 0;
console.log(score); // 0Key difference from ||:
The || operator returns the right side for any falsy value (null, undefined, 0, '', false, NaN). The ?? operator only triggers on null and undefined:
// || treats 0, '', false as "missing"
0 || 42; // 42 (0 is falsy)
'' || 'hello'; // 'hello' ('' is falsy)
false || true; // true (false is falsy)
// ?? only treats null/undefined as "missing"
0 ?? 42; // 0 (0 is NOT null/undefined)
'' ?? 'hello'; // '' ('' is NOT null/undefined)
false ?? true; // false (false is NOT null/undefined)Combining with optional chaining:
const city = user?.address?.city ?? 'Unknown';
// If user, address, or city is missing → 'Unknown'Logical assignment operators (ES2021):
let a = null;
a ??= 'default'; // a is now 'default' (was null)
let b = 0;
b ??= 42; // b is still 0 (0 is not null/undefined)
let c = '';
c ||= 'fallback'; // c is 'fallback' ('' is falsy)
let d = 5;
d &&= d * 2; // d is 10 (5 is truthy)Optional chaining and nullish coalescing shine in real-world scenarios.
API responses with optional fields:
// API might return partial data
const response = await fetchUser(id);
const avatar = response?.data?.user?.avatar ?? '/default-avatar.png';
const bio = response?.data?.user?.bio ?? 'No bio provided';Configuration objects with defaults:
function createApp(config) {
const port = config?.server?.port ?? 3000;
const host = config?.server?.host ?? 'localhost';
const debug = config?.debug ?? false;
console.log(`Starting on ${host}:${port}`);
}
createApp({ server: { port: 8080 } });
// Starting on localhost:8080
createApp(undefined);
// Starting on localhost:3000DOM element chains:
// Safely access nested DOM elements
const text = document.querySelector('.card')?.querySelector('.title')?.textContent ?? '';Chaining multiple ?. operators:
const users = [
{ name: 'Alice', address: { city: 'Paris' } },
{ name: 'Bob' }, // no address
null, // null entry
];
const cities = users.map(u => u?.address?.city ?? 'Unknown');
console.log(cities); // ['Paris', 'Unknown', 'Unknown']<div id="output"></div>
<script>
// Before: verbose null checks
const user1 = { name: 'Alice', address: { city: 'Paris' } };
const user2 = { name: 'Bob' };
const user3 = null;
// After: clean optional chaining + defaults
const city1 = user1?.address?.city ?? 'Unknown';
const city2 = user2?.address?.city ?? 'Unknown';
const city3 = user3?.address?.city ?? 'Unknown';
// ?? vs || with falsy values
const score = 0;
const withOr = score || 'no score'; // 'no score' (wrong!)
const withQQ = score ?? 'no score'; // 0 (correct!)
// Optional method call
const greet = user1?.sayHello?.() ?? 'No greeting method';
const output = document.getElementById('output');
output.innerHTML = `
<p>City 1: ${city1}</p>
<p>City 2: ${city2}</p>
<p>City 3: ${city3}</p>
<p>score || default: ${withOr}</p>
<p>score ?? default: ${withQQ}</p>
<p>Optional method: ${greet}</p>
`;
</script>What does `0 ?? 42` return?