Docs

3.10-Optional-Chaining

3.10 Optional Chaining Operator (?.)

Overview

The optional chaining operator (?.) allows you to safely access deeply nested object properties without having to explicitly check if each reference in the chain is valid. If any reference is null or undefined, the expression short-circuits and returns undefined instead of throwing an error.


Table of Contents

  1. The Problem It Solves
  2. Basic Syntax
  3. Types of Optional Chaining
  4. How It Works
  5. Comparison with Traditional Approaches
  6. Common Use Cases
  7. Best Practices
  8. Common Mistakes
  9. Browser Support

The Problem It Solves

Without Optional Chaining

// Accessing nested properties can throw errors
const user = {
  name: 'John',
  address: null,
};

// ❌ This throws: TypeError: Cannot read property 'city' of null
console.log(user.address.city);

// Traditional solution - verbose and repetitive
if (user && user.address && user.address.city) {
  console.log(user.address.city);
}

// Or with logical AND
const city = user && user.address && user.address.city;

With Optional Chaining

// ✅ Clean and safe
const city = user?.address?.city; // undefined (no error!)

Basic Syntax

object?.property
object?.[expression]
object?.method()
array?.[index]
SyntaxDescriptionReturns
obj?.propAccess property if obj existsValue or undefined
obj?.[expr]Access computed propertyValue or undefined
obj?.method()Call method if it existsResult or undefined
arr?.[index]Access array elementValue or undefined

Types of Optional Chaining

1. Property Access (?.)

const user = {
  profile: {
    name: 'Alice',
    avatar: {
      url: 'image.png',
    },
  },
};

// Safe property access
console.log(user?.profile?.name); // "Alice"
console.log(user?.profile?.avatar?.url); // "image.png"
console.log(user?.settings?.theme); // undefined (settings doesn't exist)
console.log(user?.profile?.bio?.text); // undefined (bio doesn't exist)

2. Bracket Notation (?.[])

const data = {
  users: ['Alice', 'Bob', 'Charlie'],
};

// Dynamic property access
const prop = 'users';
console.log(data?.[prop]); // ["Alice", "Bob", "Charlie"]
console.log(data?.['users']); // ["Alice", "Bob", "Charlie"]
console.log(data?.['admins']); // undefined

// Array element access
console.log(data?.users?.[0]); // "Alice"
console.log(data?.users?.[10]); // undefined
console.log(data?.admins?.[0]); // undefined

3. Method Calls (?.())

const calculator = {
  add: (a, b) => a + b,
  // subtract method doesn't exist
};

// Safe method calls
console.log(calculator.add?.(5, 3)); // 8
console.log(calculator.subtract?.(5, 3)); // undefined (method doesn't exist)

// With callback functions
function processData(callback) {
  // Call callback only if it's a function
  callback?.();
}

processData(() => console.log('Called!')); // "Called!"
processData(null); // No error, nothing happens

How It Works

Short-Circuit Evaluation

obj?.prop?.method?.()
     │      │       │
     ▼      ▼       ▼
   Check  Check   Check
   obj    prop    method
     │      │       │
     ▼      ▼       ▼
   null/   null/   not a
   undef?  undef?  function?
     │      │       │
     ▼      ▼       ▼
   Return  Return  Return
   undef   undef   undefined

Evaluation Flow

// This expression:
const result = a?.b?.c?.d;

// Is roughly equivalent to:
const result =
  a === null || a === undefined
    ? undefined
    : a.b === null || a.b === undefined
    ? undefined
    : a.b.c === null || a.b.c === undefined
    ? undefined
    : a.b.c.d;

Important: Only Checks for null and undefined

const obj = {
  zero: 0,
  empty: '',
  falsy: false,
};

// Optional chaining does NOT short-circuit on other falsy values
console.log(obj?.zero); // 0 (not undefined!)
console.log(obj?.empty); // "" (not undefined!)
console.log(obj?.falsy); // false (not undefined!)

// Only null and undefined cause short-circuiting
const obj2 = {
  nullVal: null,
  undefVal: undefined,
};

console.log(obj2?.nullVal?.prop); // undefined
console.log(obj2?.undefVal?.prop); // undefined

Comparison with Traditional Approaches

ApproachSyntaxBehavior on null/undefined
Direct accessobj.propThrows TypeError
Logical ANDobj && obj.propReturns falsy value or prop
Ternaryobj ? obj.prop : undefinedReturns undefined or prop
Optional chainingobj?.propReturns undefined

Detailed Comparison

const user = null;

// Direct access - THROWS ERROR
// user.name;  // TypeError!

// Logical AND - works but verbose
const name1 = user && user.name; // null

// Ternary - explicit but cumbersome
const name2 = user != null ? user.name : undefined; // undefined

// Optional chaining - clean and concise
const name3 = user?.name; // undefined

Optional Chaining vs Logical AND

const data = { value: 0 };

// Logical AND stops on ANY falsy value
console.log(data && data.value && data.value.toString()); // 0 (stops here!)

// Optional chaining only stops on null/undefined
console.log(data?.value?.toString?.()); // "0" (works correctly!)

Common Use Cases

1. API Response Handling

// API responses often have optional nested data
const response = await fetch('/api/user');
const data = await response.json();

// Safe access to nested data
const userName = data?.user?.profile?.name ?? 'Guest';
const avatarUrl = data?.user?.profile?.avatar?.url ?? '/default-avatar.png';
const permissions = data?.user?.roles?.[0]?.permissions ?? [];

2. Configuration Objects

function initApp(config) {
  // Safely access optional configuration
  const theme = config?.ui?.theme ?? 'light';
  const fontSize = config?.ui?.fontSize ?? 14;
  const enableLogging = config?.debug?.logging ?? false;

  // Safe method call for custom handlers
  config?.onInit?.();

  return { theme, fontSize, enableLogging };
}

// Works with full config
initApp({ ui: { theme: 'dark' } });

// Works with no config
initApp();

// Works with null
initApp(null);

3. DOM Element Access

// Element might not exist
const value = document.querySelector('#myInput')?.value ?? '';
const text = document.querySelector('.content')?.textContent?.trim() ?? '';

// Event delegation
document.addEventListener('click', (e) => {
  const buttonId = e.target?.closest?.('button')?.id;
  if (buttonId) {
    handleButtonClick(buttonId);
  }
});

4. Array Operations

const data = {
  users: [
    { name: 'Alice', scores: [95, 87, 92] },
    { name: 'Bob' }, // No scores array
  ],
};

// Safe array access
const firstUserFirstScore = data?.users?.[0]?.scores?.[0]; // 95
const secondUserFirstScore = data?.users?.[1]?.scores?.[0]; // undefined

// With array methods
const averageScore =
  data?.users?.[0]?.scores?.reduce?.((sum, score) => sum + score, 0) /
  (data?.users?.[0]?.scores?.length ?? 1);

5. Event Handlers and Callbacks

class EventEmitter {
  constructor() {
    this.handlers = {};
  }

  on(event, handler) {
    this.handlers[event] = handler;
  }

  emit(event, data) {
    // Safe callback invocation
    this.handlers[event]?.(data);
  }
}

const emitter = new EventEmitter();
emitter.emit('click', { x: 100 }); // No error even without handler

Best Practices

✅ Do: Use with Potentially Null Data

// Good: API responses, optional configs, user input
const userName = apiResponse?.user?.name;
const theme = userPrefs?.display?.theme;

✅ Do: Combine with Nullish Coalescing

// Provide defaults for missing values
const port = config?.server?.port ?? 3000;
const timeout = options?.timeout ?? 5000;

✅ Do: Use for Optional Callbacks

function fetchData(url, options) {
  options?.onStart?.();

  return fetch(url)
    .then((res) => {
      options?.onSuccess?.(res);
      return res;
    })
    .catch((err) => {
      options?.onError?.(err);
      throw err;
    });
}

❌ Don't: Overuse on Known Structures

// Bad: Unnecessary if you control the data structure
const user = { name: 'Alice', age: 25 };
const name = user?.name; // Unnecessary

// Good: Direct access when structure is guaranteed
const name = user.name;

❌ Don't: Use to Hide Bugs

// Bad: Masking potential programming errors
function processUser(user) {
  // If user should ALWAYS have a name, don't hide the error
  console.log(user?.name); // Hides the bug!
}

// Good: Validate and throw meaningful errors
function processUser(user) {
  if (!user?.name) {
    throw new Error('User must have a name');
  }
  console.log(user.name);
}

❌ Don't: Chain Unnecessarily

// Bad: Excessive chaining
const x = obj?.a?.b?.c?.d?.e?.f?.g;

// Consider: Restructure data or validate earlier
if (!obj?.a?.b) {
  throw new Error('Invalid object structure');
}
const x = obj.a.b.c.d.e.f.g;

Common Mistakes

1. Using with Assignment (Left-hand Side)

// ❌ WRONG: Cannot use optional chaining for assignment
user?.name = "Alice";  // SyntaxError!

// ✅ CORRECT: Check first, then assign
if (user) {
    user.name = "Alice";
}

// Or use nullish coalescing assignment
user ??= {};
user.name = "Alice";

2. Confusing with Logical AND

const obj = { count: 0 };

// These behave differently!
console.log(obj && obj.count); // 0 (falsy, but valid)
console.log(obj?.count); // 0 (correctly returns the value)

// Only optional chaining correctly handles this
const obj2 = { empty: '' };
console.log(obj2 && obj2.empty && obj2.empty.length); // "" (stops here)
console.log(obj2?.empty?.length); // 0 (correct!)

3. Forgetting It Returns undefined

// The result is undefined, not false or null
const result = null?.property;
console.log(result); // undefined
console.log(result === null); // false
console.log(result === undefined); // true

// Important for conditional checks
if (obj?.value) {
  // This also catches undefined from ?.
  // This block won't run if value is missing OR if value is falsy
}

4. Incorrect Syntax with Function Calls

const obj = {
  method: () => 'result',
};

// ❌ WRONG: This checks if method exists, not if obj exists
obj.method?.(); // Works, but doesn't protect against obj being null

// ✅ CORRECT: Chain from the start
obj?.method?.();

// Or if method definitely exists when obj exists
obj?.method();

Browser Support

BrowserVersion
Chrome80+
Firefox74+
Safari13.1+
Edge80+
Node.js14.0+

Transpilation

For older environments, Babel can transpile optional chaining:

npm install @babel/plugin-proposal-optional-chaining

Summary

FeatureSyntaxReturns
Property accessobj?.propValue or undefined
Bracket notationobj?.[key]Value or undefined
Method callobj?.method()Result or undefined
Combinedobj?.a?.b?.()Final value or undefined

Key Points

  1. Only checks for null and undefined - other falsy values don't trigger short-circuiting
  2. Returns undefined when short-circuited - not null
  3. Cannot be used for assignment - only for reading values
  4. Combine with ?? for default values
  5. Don't overuse - it can hide legitimate bugs

Next Steps

  • Practice with the examples in examples.js
  • Complete the exercises in exercises.js
  • Learn how it combines with Nullish Coalescing (??) for powerful patterns
.10 Optional Chaining - JavaScript Tutorial | DeepML