Back to Blog
JavaScriptOptional ChainingES2020Syntax

JavaScript Optional Chaining Guide

Master JavaScript optional chaining for safe property access and cleaner code.

B
Bootspring Team
Engineering
August 20, 2018
7 min read

Optional chaining (?.) provides a safe way to access nested object properties without checking each level.

Basic Syntax#

1const user = { 2 name: 'John', 3 address: { 4 street: '123 Main St', 5 city: 'Boston' 6 } 7}; 8 9// Without optional chaining 10const city1 = user && user.address && user.address.city; 11 12// With optional chaining 13const city2 = user?.address?.city; 14// 'Boston' 15 16// When property doesn't exist 17const user2 = { name: 'Jane' }; 18const city3 = user2?.address?.city; 19// undefined (no error!)

Property Access#

1const obj = { 2 prop: { 3 nested: { 4 value: 42 5 } 6 } 7}; 8 9// Deep property access 10obj?.prop?.nested?.value; // 42 11 12// Missing intermediate property 13const empty = {}; 14empty?.prop?.nested?.value; // undefined 15 16// Null or undefined 17const nullObj = null; 18nullObj?.prop; // undefined 19 20const undefinedObj = undefined; 21undefinedObj?.prop; // undefined 22 23// Only stops on null/undefined 24const objWithZero = { value: 0 }; 25objWithZero?.value; // 0 (not undefined!) 26 27const objWithEmpty = { value: '' }; 28objWithEmpty?.value; // '' (not undefined!)

Method Calls#

1const user = { 2 name: 'John', 3 greet() { 4 return `Hello, ${this.name}`; 5 } 6}; 7 8// Call method if it exists 9user.greet?.(); // 'Hello, John' 10 11// Method doesn't exist 12const user2 = { name: 'Jane' }; 13user2.greet?.(); // undefined (no error!) 14 15// Chained method calls 16const api = { 17 data: { 18 process() { 19 return { id: 1 }; 20 } 21 } 22}; 23 24api?.data?.process?.(); // { id: 1 } 25api?.missing?.process?.(); // undefined 26 27// With arguments 28const calculator = { 29 add: (a, b) => a + b 30}; 31 32calculator.add?.(2, 3); // 5 33calculator.subtract?.(2, 3); // undefined

Array Access#

1const users = [ 2 { name: 'John', email: 'john@example.com' }, 3 { name: 'Jane' } 4]; 5 6// Access array element properties 7users?.[0]?.name; // 'John' 8users?.[0]?.email; // 'john@example.com' 9users?.[1]?.email; // undefined 10users?.[5]?.name; // undefined 11 12// Nested arrays 13const data = { 14 items: [ 15 { values: [1, 2, 3] } 16 ] 17}; 18 19data?.items?.[0]?.values?.[0]; // 1 20data?.items?.[1]?.values?.[0]; // undefined 21 22// Dynamic index 23const index = 0; 24users?.[index]?.name; // 'John'

With Nullish Coalescing#

1// Combine with ?? for default values 2const user = { name: 'John' }; 3 4const city = user?.address?.city ?? 'Unknown'; 5// 'Unknown' 6 7// Different from || for falsy values 8const settings = { volume: 0, brightness: '' }; 9 10// || treats 0 and '' as falsy 11settings?.volume || 100; // 100 (wrong!) 12settings?.brightness || 50; // 50 (wrong!) 13 14// ?? only checks null/undefined 15settings?.volume ?? 100; // 0 (correct!) 16settings?.brightness ?? 50; // '' (correct!) 17 18// Nested with defaults 19const config = {}; 20const port = config?.server?.port ?? 3000; 21// 3000

Function Parameters#

1function getNestedValue(obj) { 2 return obj?.data?.values?.[0]?.name; 3} 4 5getNestedValue({ data: { values: [{ name: 'test' }] } }); 6// 'test' 7 8getNestedValue({ data: {} }); 9// undefined 10 11getNestedValue(null); 12// undefined 13 14// With destructuring 15function processUser({ user } = {}) { 16 const name = user?.profile?.name ?? 'Guest'; 17 return name; 18} 19 20processUser({ user: { profile: { name: 'John' } } }); // 'John' 21processUser({ user: {} }); // 'Guest' 22processUser({}); // 'Guest' 23processUser(); // 'Guest'

Object Methods#

1// Optional method on potentially undefined object 2const response = { 3 data: { 4 json() { 5 return { id: 1 }; 6 } 7 } 8}; 9 10response.data?.json?.(); // { id: 1 } 11response.error?.json?.(); // undefined 12 13// Callback execution 14function fetchData(callback) { 15 const data = { id: 1 }; 16 callback?.(data); // Only calls if callback exists 17} 18 19fetchData(console.log); // Logs { id: 1 } 20fetchData(); // Does nothing, no error

Delete Operator#

1const obj = { 2 prop: { 3 nested: 'value' 4 } 5}; 6 7// Delete with optional chaining 8delete obj?.prop?.nested; // true 9console.log(obj.prop); // {} 10 11// Safe delete on missing property 12delete obj?.missing?.prop; // true (no error)

Short-Circuiting#

1// Optional chaining short-circuits 2const obj = null; 3let called = false; 4 5obj?.method(called = true); 6console.log(called); // false - never evaluated! 7 8// Side effects won't run 9let x = 0; 10const data = null; 11data?.[x++]; 12console.log(x); // 0 - x++ never executed 13 14// Function calls that would throw 15const config = null; 16config?.validate?.(); // undefined, no error 17 18// Array methods 19const list = null; 20list?.map?.(x => x * 2); // undefined 21list?.filter?.(x => x > 0); // undefined

Class Properties#

1class User { 2 constructor(data) { 3 this.profile = data?.profile; 4 this.settings = data?.settings; 5 } 6 7 getName() { 8 return this.profile?.name ?? 'Anonymous'; 9 } 10 11 getEmail() { 12 return this.profile?.contact?.email; 13 } 14} 15 16const user1 = new User({ profile: { name: 'John' } }); 17user1.getName(); // 'John' 18user1.getEmail(); // undefined 19 20const user2 = new User(null); 21user2.getName(); // 'Anonymous' 22user2.getEmail(); // undefined

API Response Handling#

1async function fetchUser(id) { 2 try { 3 const response = await fetch(`/api/users/${id}`); 4 const data = await response.json(); 5 6 return { 7 name: data?.user?.name ?? 'Unknown', 8 email: data?.user?.email ?? 'N/A', 9 avatar: data?.user?.profile?.avatar?.url, 10 friends: data?.user?.friends?.map(f => f.name) ?? [] 11 }; 12 } catch (error) { 13 return null; 14 } 15} 16 17// Safe access to response data 18async function getFirstPost(userId) { 19 const response = await fetch(`/api/users/${userId}/posts`); 20 const data = await response.json(); 21 22 return data?.posts?.[0]?.title ?? 'No posts'; 23}

Event Handling#

1// DOM events 2document.querySelector('.button')?.addEventListener('click', handler); 3 4// React-style event handling 5function handleClick(event) { 6 const value = event?.target?.value; 7 const dataset = event?.target?.dataset?.id; 8 const parent = event?.target?.parentElement?.id; 9 10 console.log({ value, dataset, parent }); 11} 12 13// Optional callback execution 14function Component({ onClick, onHover }) { 15 return { 16 handleClick: (e) => onClick?.(e), 17 handleHover: (e) => onHover?.(e) 18 }; 19}

Common Patterns#

1// Safe JSON parsing 2function safeJSONParse(str) { 3 try { 4 return JSON.parse(str); 5 } catch { 6 return null; 7 } 8} 9 10const data = safeJSONParse(input)?.result?.value ?? defaultValue; 11 12// Configuration access 13const config = loadConfig(); 14const apiUrl = config?.api?.baseUrl ?? 'http://localhost:3000'; 15const timeout = config?.api?.timeout ?? 5000; 16 17// Feature flags 18const features = getFeatures(); 19const isEnabled = features?.newDashboard?.enabled ?? false; 20 21// Nested form data 22const formData = { 23 user: { 24 addresses: [{ 25 city: 'Boston' 26 }] 27 } 28}; 29 30const city = formData?.user?.addresses?.[0]?.city ?? '';

When NOT to Use#

1// Don't overuse for required properties 2// If user is always required: 3// ❌ user?.name (implies user might not exist) 4// ✓ user.name (crashes if invariant violated) 5 6// Don't use for error hiding 7// ❌ 8try { 9 const value = dangerousOperation()?.result; 10} catch { 11 // Hidden errors 12} 13 14// ✓ 15const result = dangerousOperation(); 16if (result) { 17 const value = result.value; 18} 19 20// Don't chain unnecessarily 21// ❌ obj?.foo?.bar?.baz when obj.foo.bar is always present 22// ✓ obj.foo.bar?.baz when only baz might be missing

TypeScript Integration#

1interface User { 2 name: string; 3 address?: { 4 street?: string; 5 city: string; 6 }; 7} 8 9function getCity(user: User | undefined): string { 10 // TypeScript understands optional chaining 11 return user?.address?.city ?? 'Unknown'; 12} 13 14// Works with strict null checks 15function processUser(user?: User) { 16 const street = user?.address?.street; 17 // street: string | undefined 18}

Best Practices#

Use Optional Chaining When: ✓ Property might not exist ✓ Object might be null/undefined ✓ Accessing API response data ✓ Working with optional config Combine With: ✓ Nullish coalescing (??) for defaults ✓ Try-catch for actual errors ✓ Type guards for type narrowing Avoid: ✗ Hiding genuine errors ✗ Overusing on required properties ✗ Using || instead of ?? for defaults ✗ Excessive chaining depth Performance: ✓ No significant overhead ✓ Short-circuits efficiently ✓ Same as manual null checks

Conclusion#

Optional chaining (?.) simplifies safe property access and eliminates verbose null checks. Use it with nullish coalescing (??) for default values, but don't overuse it to hide errors or on properties that should always exist. It's perfect for API responses, configuration objects, and any data where properties might be missing.

Share this article

Help spread the word about Bootspring