Learning Objectives
- Use optional chaining (?.) for safe property access
- Apply nullish coalescing (??) for default values
- Chain optional calls and array access
- Combine with other operators
- Avoid common pitfalls
The Problem: Accessing Nested Properties
const user = {
name: 'John',
address: {
street: '123 Main St',
city: 'New York'
}
};
// This works
console.log(user.address.city); // 'New York'
// But this crashes
const user2 = { name: 'Jane' };
console.log(user2.address.city); // TypeError: Cannot read property 'city' of undefined
Old Solution: Manual Checks
// Verbose and repetitive
const city = user2 && user2.address && user2.address.city;
// Or with if statements
let city;
if (user2 && user2.address) {
city = user2.address.city;
}
Optional Chaining (?.) to the Rescue
Optional chaining short-circuits and returns undefined if any part is null/undefined:
const user = { name: 'Jane' };
// No error! Returns undefined
const city = user?.address?.city;
console.log(city); // undefined
// Works when property exists
const user2 = {
name: 'John',
address: { city: 'New York' }
};
console.log(user2?.address?.city); // 'New York'
Optional Chaining Syntax
Property Access
obj?.prop
obj?.[expr]
Function Calls
func?.()
obj.method?.()
Array Access
arr?.[index]
Real-World Examples
API Response Handling
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Safe access to nested properties
const userName = data?.user?.name;
const avatar = data?.user?.profile?.avatar;
const firstHobby = data?.user?.hobbies?.[0];
return { userName, avatar, firstHobby };
}
Optional Method Calls
const user = {
name: 'John',
greet() {
return `Hello, ${this.name}!`;
}
};
// Call method if it exists
console.log(user.greet?.()); // "Hello, John!"
console.log(user.farewell?.()); // undefined (no error!)
Event Handlers
// Safe callback invocation
function processData(data, callback) {
const result = transform(data);
callback?.(result); // Only calls if callback exists
}
// Usage
processData(data); // No error even without callback
processData(data, (result) => console.log(result)); // Calls callback
DOM Manipulation
// Safe DOM access
const button = document.querySelector('#submit-btn');
button?.addEventListener('click', handleClick);
// Chain multiple optional accesses
const text = document.querySelector('.container')?.querySelector('.text')?.textContent;
Nullish Coalescing (??)
Returns the right operand when the left is null or undefined:
const value = null ?? 'default';
console.log(value); // 'default'
const value2 = undefined ?? 'default';
console.log(value2); // 'default'
const value3 = 0 ?? 'default';
console.log(value3); // 0 (not 'default'!)
const value4 = '' ?? 'default';
console.log(value4); // '' (not 'default'!)
?? vs ||
// || treats 0, '', false as falsy
const count = 0;
console.log(count || 10); // 10 (wrong!)
console.log(count ?? 10); // 0 (correct!)
const name = '';
console.log(name || 'Guest'); // 'Guest' (wrong!)
console.log(name ?? 'Guest'); // '' (correct!)
// Use || for actual falsy checks
const name = '';
console.log(name || 'Guest'); // 'Guest' (correct for this case!)
Combining ?. and ??
const user = { name: 'John' };
// Get city or default
const city = user?.address?.city ?? 'Unknown';
console.log(city); // 'Unknown'
// Get first hobby or default
const hobby = user?.hobbies?.[0] ?? 'No hobbies';
console.log(hobby); // 'No hobbies'
Configuration with Defaults
function createServer(config) {
const port = config?.server?.port ?? 3000;
const host = config?.server?.host ?? 'localhost';
const timeout = config?.timeout ?? 5000;
return { port, host, timeout };
}
// All defaults
createServer();
// { port: 3000, host: 'localhost', timeout: 5000 }
// Partial config
createServer({ server: { port: 8080 } });
// { port: 8080, host: 'localhost', timeout: 5000 }
Array and Function Examples
Optional Array Access
const users = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
];
// Safe array access
const firstUser = users?.[0];
const thirdUser = users?.[2]; // undefined, no error
// Chain with property access
const firstName = users?.[0]?.name; // 'John'
const thirdName = users?.[2]?.name; // undefined
Dynamic Property Access
const user = { name: 'John', age: 30 };
const prop = 'address';
// Safe dynamic access
const value = user?.[prop]?.city;
console.log(value); // undefined
Optional Function Execution
const api = {
fetchUser: (id) => ({ id, name: 'John' })
};
// Call method if it exists
const user = api.fetchUser?.(1);
const posts = api.fetchPosts?.(1); // undefined, no error
// With async
const data = await api.fetchData?.();
Common Patterns
Form Validation
function validateForm(form) {
const email = form?.elements?.email?.value ?? '';
const password = form?.elements?.password?.value ?? '';
return email && password;
}
localStorage with Fallback
const theme = localStorage.getItem('theme') ?? 'light';
const user = JSON.parse(localStorage.getItem('user') ?? '{}');
Environment Variables
const config = {
apiUrl: process.env?.API_URL ?? 'http://localhost:3000',
debug: process.env?.DEBUG === 'true' ?? false
};
Common Pitfalls
Pitfall 1: Overusing optional chaining
// Too defensive - user should always exist
function greet(user) {
return `Hello, ${user?.name}!`; // Unnecessary
}
// Better - fail fast if user is missing
function greet(user) {
return `Hello, ${user.name}!`;
}
Pitfall 2: Hiding bugs
// Silently returns undefined - hard to debug
const result = calculateTotal?.();
// Better - be explicit about optional behavior
if (typeof calculateTotal === 'function') {
const result = calculateTotal();
} else {
console.warn('calculateTotal is not a function');
}
Pitfall 3: Confusing ?? with ||
const count = 0;
// Wrong - treats 0 as falsy
const value = count || 10; // 10
// Right - only null/undefined
const value = count ?? 10; // 0
Best Practices
1. Use for external data
Optional chaining is perfect for API responses and user input:
const userName = apiResponse?.data?.user?.name ?? 'Guest';
2. Don't hide required properties
If a property should always exist, don't use optional chaining:
// Bad - hides bugs
function processUser(user) {
return user?.id; // Should error if user is missing
}
// Good - fails fast
function processUser(user) {
return user.id;
}
3. Combine with destructuring
const { name, email } = user?.profile ?? {};
Browser Support
Optional chaining and nullish coalescing are supported in:
- Chrome 80+
- Firefox 74+
- Safari 13.1+
- Edge 80+
- Node.js 14+
For older browsers, use Babel to transpile.
Key Takeaways
- Optional chaining (?.) safely accesses nested properties
- Returns undefined instead of throwing errors
- Works with properties, methods, and arrays
- Nullish coalescing (??) provides defaults for null/undefined
- ?? only checks null/undefined, not all falsy values
- Combine ?. and ?? for safe access with defaults
- Don't overuse - let required properties fail fast
- Perfect for external data and optional callbacks