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:

For older browsers, use Babel to transpile.

Key Takeaways