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); // undefinedArray 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// 3000Function 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 errorDelete 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); // undefinedClass 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(); // undefinedAPI 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 missingTypeScript 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.