| |

JavaScript 56 🧬 Optional chaining ?.

⏱️ Estimated read: 15 min | |
const user = {
  name: 'Alice',
  address: {
    city: 'Paris',
    zip: '75001'
  }
};

console.log(user?.name);
console.log(user?.address?.city);
console.log(user?.address?.zip);
console.log(user?.phone?.number);

const obj = null;
console.log(obj?.name);

const arr = [1, 2, 3];
console.log(arr?.[0]);
console.log(arr?.[10]);

const fn = null;
console.log(fn?.());

const api = {
  getName() { return 'Alice'; }
};

console.log(api.getName?.());
console.log(api.getAge?.());

const config = {
  port: 0,
  host: null,
  debug: false
};

console.log(config?.host ?? 'localhost');
console.log(config?.port ?? 8080);
console.log(config?.debug ?? true);

let user1;
console.log(user1?.profile?.name ?? 'Guest');

Optional chaining (?.) lets you safely access deeply nested properties without checking every level. If any link in the chain is null or undefined, the whole expression short-circuits and returns undefined — instead of throwing a TypeError.

Key point: ?. short-circuits the entire expression the moment it hits null or undefined. It doesn’t just skip one step — it stops everything after it, including function calls and array access. This makes it the cleanest way to safely dig into unknown data.


a – What is optional chaining

Optional chaining is a syntax for safely reading nested properties. Instead of writing guard after guard, you chain with ?. and let it fail gracefully.

The problem it solves:

Without ?., accessing a nested property on a null or undefined value throws:

const user = null;

console.log(user.name);
// TypeError: Cannot read properties of null (reading 'name')

The old workaround was verbose:

const name = user && user.address && user.address.city;

With ?.:

const name = user?.address?.city;

If user is null or undefined, the whole expression evaluates to undefined — no error.

The three forms:

FormPurposeExample
obj?.propProperty accessuser?.name
obj?.[key]Dynamic propertyuser?.[key]
func?.()Function callfn?.()

All three short-circuit on null or undefined.

Property access (?.):

const user = {
  name: 'Alice',
  address: {
    city: 'Paris'
  }
};

console.log(user?.name);
// [ 'Alice' ]

console.log(user?.address?.city);
// [ 'Paris' ]

console.log(user?.phone?.number);
// [ undefined ]

user?.phone is undefined, so ?.number short-circuits and returns undefined.

Dynamic property access (?.[]):

const obj = { key: 'value' };
const key = 'key';

console.log(obj?.[key]);
// [ 'value' ]

const empty = null;
console.log(empty?.[key]);
// [ undefined ]

Use ?.[] when the key comes from a variable.

Function call (?.()):

const api = {
  getName() { return 'Alice'; }
};

console.log(api.getName?.());
// [ 'Alice' ]

console.log(api.getAge?.());
// [ undefined ]

If the method exists, it’s called. If not, the whole expression is undefined.

Chaining:

const city = user?.address?.city?.toUpperCase();

Every step is protected. If any is null or undefined, the result is undefined.

Short-circuit behavior:

Once ?. hits null or undefined, everything after it is skipped:

const user = null;

console.log(user?.address.city);
// [ undefined ]  ← address.city is never evaluated

Even though .city doesn’t have a ?., it’s never reached because user?. short-circuited.

This applies to function calls too:

const obj = null;

console.log(obj?.method());
// [ undefined ]  ← method() is never called

Why ?. matters:

  • Concise — no more && chains
  • Safe — no more TypeErrors from nested access
  • Readable — intent is clear
  • Composable — works with ??, [], and calls
  • Standard — part of ES2020, supported everywhere modern

Optional chaining vs &&:

// Old way
const city = user && user.address && user.address.city;

// New way
const city = user?.address?.city;

The ?. version is shorter, clearer, and less error-prone.

?. vs ?. — same operator, different contexts:

The operator is ?. — a dot preceded by a question mark. Where you place it determines the form:

obj?.prop          // property
obj?.[key]         // dynamic property
obj?.method()      // method call

Warning: Don’t write ? . with a space — that’s a syntax error. And don’t confuse ?. with the ternary ? :.


b – Short-circuiting and common patterns

The short-circuit behavior is what makes ?. powerful. Understanding it unlocks cleaner code.

The rule: The moment ?. sees null or undefined, it returns undefined and stops evaluating.

Whole chain stops:

const user = null;
console.log(user?.address?.city?.zip?.plus4);
// [ undefined ]

Only the first ?. matters here — the rest never run.

Function calls don’t happen:

let calls = 0;
const obj = {
  method() {
    calls++;
    return 'called';
  }
};

const nullObj = null;
console.log(nullObj?.method());
// [ undefined ]

console.log(calls);
// [ 0 ]  ← method never ran

Array indices:

const arr = [1, 2, 3];

console.log(arr?.[0]);
// [ 1 ]

const nullArr = null;
console.log(nullArr?.[0]);
// [ undefined ]

Combining with ??:

The two operators are natural partners — ?. for safe access, ?? for defaults:

const user = {
  profile: null
};

const city = user.profile?.address?.city ?? 'Unknown';
console.log(city);
// [ 'Unknown' ]

Pattern 1 — Safe nested access:

function getStreet(user) {
  return user?.address?.street ?? 'No address';
}

Pattern 2 — Optional methods:

element?.addEventListener?.('click', handler);

Calls the method only if it exists.

Pattern 3 — Safe callbacks:

function run(callback) {
  callback?.();
}

run();                              // no error
run(() => console.log('called'));   // called

Pattern 4 — Optional config:

function setup(config) {
  const port = config?.port ?? 8080;
  const host = config?.host ?? 'localhost';
  return { port, host };
}

Pattern 5 — Event handlers:

onSuccess?.(result);
onError?.(error);

Common in libraries where hooks are optional.

Pattern 6 — JSON or API responses:

const name = response?.data?.user?.name ?? 'Anonymous';

Pattern 7 — Array of objects:

const users = [{ name: 'Alice' }, null, { name: 'Bob' }];
const names = users.map(u => u?.name ?? 'Unknown');
// [ [ 'Alice', 'Unknown', 'Bob' ] ]

Pattern 8 — Cleanup handlers:

const cleanup = () => console.log('cleanup');
cleanup?.();

Pattern 9 — Chained methods:

const result = value?.toString?.().toUpperCase?.();

Rare but valid — every step is guarded.

Pattern 10 — Options objects:

function render(element, options) {
  const width = options?.width ?? 100;
  const height = options?.height ?? 100;
  // ...
}

What ?. does NOT do:

?. only checks for null and undefined. Other falsy values (0, '', false, NaN) don’t short-circuit:

const obj = { count: 0 };

console.log(obj?.count);
// [ 0 ]  ← 0 doesn't short-circuit

console.log(obj?.count?.toString?.());
// [ '0' ]

0 isn’t nullish, so the chain continues.

Common mistake — no short-circuit fallback:

const value = obj?.a?.b ?? 'default';

This uses ?? to catch the undefined that ?. returns. Without ??, you’d just get undefined.

Comparison table:

ExpressionWhen obj is nullWhen obj is {a: 1}
obj.aTypeError1
obj?.aundefined1
obj?.a?.bundefinedundefined
obj?.method()undefinedcall result
obj?.[k]undefinedobj[k]

Short-circuit gotcha — ?. doesn’t protect assignments:

const obj = null;
obj?.prop = 'value';
// TypeError: Cannot set properties of null

?. works for reads, not writes. To conditionally assign:

if (obj != null) obj.prop = 'value';

delete with ?.:

const obj = null;
delete obj?.prop;
// [ true ]  ← delete is a no-op, no error

delete short-circuits safely.

Optional chaining is not a null check replacement:

// ❌ Not equivalent
if (obj?.prop) { ... }   // false if prop is 0, '', false

// ✅ Better
if (obj?.prop != null) { ... }

?. returns undefined on short-circuit — but a value like 0 still fails a truthiness check.


c – Real-world uses of optional chaining

Where ?. earns its keep in production code.

Pattern 1 — API response validation:

function getUsername(response) {
  return response?.data?.user?.username ?? 'guest';
}

Pattern 2 — Config with defaults:

function configure(options = {}) {
  return {
    host: options?.host ?? '0.0.0.0',
    port: options?.port ?? 3000,
    debug: options?.debug ?? false
  };
}

Pattern 3 — Optional callbacks in components:

class Component {
  constructor(props) {
    this.onMount = props?.onMount;
    this.onUnmount = props?.onUnmount;
  }
  mount() {
    this.onMount?.();
  }
  unmount() {
    this.onUnmount?.();
  }
}

Pattern 4 — Safe method calls on DOM:

const el = document.querySelector('.missing');
el?.addEventListener('click', handler);

No error if the element isn’t found.

Pattern 5 — Tree traversal:

function getValue(tree, path) {
  let current = tree;
  for (const key of path) {
    current = current?.[key];
  }
  return current;
}

const tree = { a: { b: { c: 42 } } };
console.log(getValue(tree, ['a', 'b', 'c']));
// [ 42 ]

console.log(getValue(tree, ['a', 'x', 'c']));
// [ undefined ]

Pattern 6 — Optional constructor arguments:

function createUser(input) {
  return {
    name: input?.name ?? 'Anonymous',
    email: input?.email ?? null,
    age: input?.age ?? 0
  };
}

Pattern 7 — Iterating optional arrays:

const items = data?.items ?? [];

items.forEach(item => console.log(item));

If data.items is missing, items becomes an empty array — no crash.

Pattern 8 — Optional event fields:

function handleClick(event) {
  const x = event?.clientX ?? 0;
  const y = event?.clientY ?? 0;
  console.log(`clicked at ${x}, ${y}`);
}

Pattern 9 — Nested map/object lookups:

const lookup = {
  users: {
    alice: { id: 1, name: 'Alice' }
  }
};

function getUser(username) {
  return lookup?.users?.[username] ?? null;
}

console.log(getUser('alice'));
// [ { id: 1, name: 'Alice' } ]

console.log(getUser('bob'));
// [ null ]

Pattern 10 — Version strings:

const version = process?.versions?.node ?? 'unknown';

Useful in environments where process might not exist.

Pattern 11 — Optional chaining in JSX (React):

function Profile({ user }) {
  return <h1>{user?.name ?? 'Guest'}</h1>;
}

Pattern 12 — Safe array access:

const first = arr?.[0];
const last = arr?.[arr.length - 1];

Pattern 13 — Access on optional promise results:

const result = await fetchData?.();
const name = result?.user?.name;

Pattern 14 — Optional modules:

const config = require('config')?.default ?? {};

Pattern 15 — Safe property chains on unknown data:

function extractCity(order) {
  return order?.shipping?.address?.city ?? 'N/A';
}

Pattern 16 — Optional Map lookups:

const map = new Map();
const value = map?.get?.('key') ?? 'default';

Pattern 17 — Optional chaining with destructuring:

const { name = 'Anonymous' } = user?.profile ?? {};

Pattern 18 — Optional array of options:

const first = options?.items?.[0]?.value ?? 'default';

Pattern 19 — Cascading fallbacks:

function getTheme(user) {
  return user?.preferences?.theme
      ?? user?.settings?.theme
      ?? 'light';
}

Pattern 20 — Full example:

function renderUserCard(data) {
  const name = data?.user?.name ?? 'Anonymous';
  const avatar = data?.user?.avatar ?? '/default.png';
  const city = data?.user?.address?.city ?? 'Unknown';
  const bio = data?.user?.bio ?? '';

  return { name, avatar, city, bio };
}

console.log(renderUserCard({}));
// [ { name: 'Anonymous', avatar: '/default.png', city: 'Unknown', bio: '' } ]

What ?. replaces:

// Before
const name = user && user.profile && user.profile.name;

// After
const name = user?.profile?.name;

What ?. enables with ??:

const name = user?.profile?.name ?? 'Guest';

When to use ?.:

SituationUse
API responses✅
Optional config✅
Optional callbacks✅
Known-safe objects⚠️ Not needed
Hot loops⚠️ Minor overhead

When NOT to use ?.:

SituationWhy
You expect a valueFail fast is better
Real bugs?. hides them
Missing required dataThrow a clear error
Deeply nested accessRefactor instead

?. can hide bugs:

// ❌ Silent failure
const total = order?.items?.reduce(...) ?? 0;

// If `order` is required but missing, you get 0 instead of an error

If a value should exist, let it throw. Use ?. only when the absence is expected.


Complete Example Session

// ============================================
// PART 1: BASIC PROPERTY ACCESS
// ============================================

const user = {
  name: 'Alice',
  address: { city: 'Paris', zip: '75001' }
};

console.log(user?.name);
// [ 'Alice' ]

console.log(user?.address?.city);
// [ 'Paris' ]

console.log(user?.phone?.number);
// [ undefined ]

// ============================================
// PART 2: NULL ROOT
// ============================================

const obj = null;
console.log(obj?.name);
// [ undefined ]

// ============================================
// PART 3: ARRAY ACCESS
// ============================================

const arr = [1, 2, 3];
console.log(arr?.[0]);
// [ 1 ]

console.log(arr?.[10]);
// [ undefined ]

const nullArr = null;
console.log(nullArr?.[0]);
// [ undefined ]

// ============================================
// PART 4: FUNCTION CALL
// ============================================

const api = {
  getName() { return 'Alice'; }
};

console.log(api.getName?.());
// [ 'Alice' ]

console.log(api.getAge?.());
// [ undefined ]

// ============================================
// PART 5: NULL FUNCTION
// ============================================

const fn = null;
console.log(fn?.());
// [ undefined ]

// ============================================
// PART 6: SHORT-CIRCUIT
// ============================================

const nullUser = null;
console.log(nullUser?.address?.city?.zip);
// [ undefined ]

// ============================================
// PART 7: CALL NOT MADE
// ============================================

let calls = 0;
const obj2 = {
  method() { calls++; return 'called'; }
};

const nullObj = null;
console.log(nullObj?.method());
// [ undefined ]

console.log(calls);
// [ 0 ]

// ============================================
// PART 8: WITH NULLISH COALESCING
// ============================================

const config = { port: 0, host: null };

console.log(config?.host ?? 'localhost');
// [ 'localhost' ]

console.log(config?.port ?? 8080);
// [ 0 ]

// ============================================
// PART 9: DYNAMIC KEY
// ============================================

const obj3 = { key: 'value' };
const key = 'key';

console.log(obj3?.[key]);
// [ 'value' ]

// ============================================
// PART 10: SAFE CALLBACK
// ============================================

function run(cb) {
  cb?.();
}

run();
console.log('after');
// [ 'after' ]

run(() => console.log('called'));
// [ 'called' ]

// ============================================
// PART 11: NESTED CHAIN
// ============================================

let user1;
console.log(user1?.profile?.name ?? 'Guest');
// [ 'Guest' ]

// ============================================
// PART 12: METHOD NOT PRESENT
// ============================================

const mod = {};
console.log(mod.method?.());
// [ undefined ]

// ============================================
// PART 13: ARRAY OF OBJECTS
// ============================================

const users = [{ name: 'Alice' }, null, { name: 'Bob' }];
console.log(users.map(u => u?.name ?? 'Unknown'));
// [ [ 'Alice', 'Unknown', 'Bob' ] ]

// ============================================
// PART 14: NOT SHORT-CIRCUITED BY FALSY
// ============================================

const data = { count: 0 };

console.log(data?.count);
// [ 0 ]

console.log(data?.count?.toString?.());
// [ '0' ]

// ============================================
// PART 15: ASSIGNMENT FAILS
// ============================================

try {
  const nullObj2 = null;
  nullObj2?.prop = 'value';
} catch (err) {
  console.log(err.message);
}
// [ Cannot set properties of null (setting 'prop') ]

// ============================================
// PART 16: DELETE SAFE
// ============================================

const nullObj3 = null;
console.log(delete nullObj3?.prop);
// [ true ]

// ============================================
// PART 17: OPTIONAL MODULE
// ============================================

const config3 = { port: 3000 };
const settings = config3?.default ?? config3;
console.log(settings.port);
// [ 3000 ]

// ============================================
// PART 18: TREE WALK
// ============================================

function getValue(tree, path) {
  let current = tree;
  for (const key of path) {
    current = current?.[key];
  }
  return current;
}

const tree = { a: { b: { c: 42 } } };
console.log(getValue(tree, ['a', 'b', 'c']));
// [ 42 ]

console.log(getValue(tree, ['a', 'x', 'c']));
// [ undefined ]

// ============================================
// PART 19: DESTRUCTURING WITH DEFAULTS
// ============================================

const { name = 'Anonymous' } = user?.profile ?? {};
console.log(name);
// [ 'Anonymous' ]

// ============================================
// PART 20: FULL SCRIPT
// ============================================

const user56 = {
  name: 'Alice',
  address: { city: 'Paris', zip: '75001' }
};

console.log(user56?.name);
console.log(user56?.address?.city);
console.log(user56?.address?.zip);
console.log(user56?.phone?.number);

const obj56 = null;
console.log(obj56?.name);

const arr56 = [1, 2, 3];
console.log(arr56?.[0]);
console.log(arr56?.[10]);

const fn56 = null;
console.log(fn56?.());

const api56 = {
  getName() { return 'Alice'; }
};

console.log(api56.getName?.());
console.log(api56.getAge?.());

const config56 = { port: 0, host: null, debug: false };

console.log(config56?.host ?? 'localhost');
console.log(config56?.port ?? 8080);
console.log(config56?.debug ?? true);

let user1_56;
console.log(user1_56?.profile?.name ?? 'Guest');

Quick Reference

The Three Forms

FormUse forExample
?.Propertyuser?.name
?.[]Dynamic keyobj?.[key]
?.()Function callfn?.()

Short-Circuit Rules

RuleEffect
Null rootReturns undefined
Undefined rootReturns undefined
Falsy root (0, ”, false)Continues chain
Whole chain stopsa?.b.c.d never reaches c or d
Function not calledobj?.fn() skips call

vs Alternatives

PatternOld wayNew way
Propertya && a.ba?.b
Callfn && fn()fn?.()
Arraya && a[i]a?.[i]
Defaulta && a.b || ca?.b ?? c

?. + ??

ExpressionResult
a?.b ?? 'x''x' if missing
a?.b?.c ?? 'x''x' if any missing
fn?.() ?? 'x''x' if fn missing

Works With

FeatureSupported
Object properties✅
Array indices✅
Function calls✅
Optional methods✅
delete✅
Destructuring✅
Assignments❌

Does Not Short-Circuit On

Value?. behavior
0Continues
''Continues
falseContinues
NaNContinues
nullShort-circuits
undefinedShort-circuits

Gotchas

IssueSolution
obj?.prop = xDoesn’t protect assignments
if (obj?.prop)Falsy values break truthiness
a?.[key]Use bracket for dynamic key
a?. bSpace is syntax error
a ?. b : cNot the ternary — space matters

Best Practices

✅ Do This:

// Safe nested access
const city = user?.address?.city;               // ✅

// With fallback
const name = user?.profile?.name ?? 'Guest';    // ✅

// Optional callbacks
callback?.();                                    // ✅

// Optional methods
obj.method?.();                                  // ✅

// Dynamic keys
obj?.[key];                                      // ✅

// Array access
arr?.[0];                                        // ✅

// API responses
data?.items?.forEach(...);                       // ✅

// Deleting optional props
delete obj?.prop;                                // ✅

❌ Don’t Do This:

// Don't overuse — hides bugs
const name = user?.profile?.name;                // ⚠️  if user is required

// Don't rely on truthiness after ?.
if (obj?.count) { ... }                          // ❌ fails for 0

// Don't expect assignments to be protected
obj?.prop = 'x';                                 // ❌ TypeError if null

// Don't chain with a space
obj ?. prop;                                     // ❌ SyntaxError

// Don't confuse with ternary
obj ?.method() : other;                          // ❌ wrong syntax

// Don't chain for deeply nested known data
const x = a?.b?.c?.d?.e;                         // ⚠️  refactor instead

// Don't ignore undefined
const val = obj?.value;                          // ✅
console.log(val + 1);                            // ❌ NaN if undefined

Common Pitfalls

PitfallProblemSolution
Space before ?.SyntaxErrorNo space
Assignment with ?.TypeErrorCheck null first
Truthiness after ?.Falsy values failUse != null
Confusing with ternarySyntax error?. is one operator
OverusingHides bugsUse only when absent is OK
Not adding ??Just undefinedAdd fallback
0 and ''Not short-circuitedExpected
Deep chainsPerformance costRefactor

Real-World Examples

1. Basic Property

const user = { name: 'Alice' };
console.log(user?.name);
// [ 'Alice' ]

2. Null Root

const user = null;
console.log(user?.name);
// [ undefined ]

3. Nested Access

const user = { address: { city: 'Paris' } };
console.log(user?.address?.city);
// [ 'Paris' ]

4. Missing Intermediate

const user = {};
console.log(user?.address?.city);
// [ undefined ]

5. Array Index

const arr = [1, 2, 3];
console.log(arr?.[0]);
// [ 1 ]

6. Missing Array

const arr = null;
console.log(arr?.[0]);
// [ undefined ]

7. Dynamic Key

const obj = { key: 'value' };
const key = 'key';
console.log(obj?.[key]);
// [ 'value' ]

8. Function Call

const fn = () => 'called';
console.log(fn?.());
// [ 'called' ]

9. Null Function

const fn = null;
console.log(fn?.());
// [ undefined ]

10. Optional Method

const api = { getName: () => 'Alice' };
console.log(api.getName?.());
// [ 'Alice' ]

11. Missing Method

const api = {};
console.log(api.getName?.());
// [ undefined ]

12. With Nullish Coalescing

const user = null;
console.log(user?.name ?? 'Guest');
// [ 'Guest' ]

13. Optional Callback

function run(cb) { cb?.(); }
run();
// (no output)

run(() => console.log('called'));
// [ 'called' ]

14. Short-Circuit

const user = null;
console.log(user?.a?.b?.c);
// [ undefined ]

15. Falsy Continues

const obj = { count: 0 };
console.log(obj?.count);
// [ 0 ]

16. With Optional Chaining

const city = user?.address?.city ?? 'Unknown';
console.log(city);
// [ 'Unknown' ]

17. Array of Objects

const users = [{ name: 'Alice' }, null];
console.log(users.map(u => u?.name ?? 'Unknown'));
// [ [ 'Alice', 'Unknown' ] ]

18. Tree Walk

function get(tree, path) {
  let cur = tree;
  for (const k of path) cur = cur?.[k];
  return cur;
}

console.log(get({ a: { b: 42 } }, ['a', 'b']));
// [ 42 ]

19. Destructuring

const { name = 'Anonymous' } = user?.profile ?? {};
console.log(name);
// [ 'Anonymous' ]

20. Full Script

const user56 = {
  name: 'Alice',
  address: { city: 'Paris', zip: '75001' }
};

console.log(user56?.name);
console.log(user56?.address?.city);
console.log(user56?.address?.zip);
console.log(user56?.phone?.number);

const obj56 = null;
console.log(obj56?.name);

const arr56 = [1, 2, 3];
console.log(arr56?.[0]);
console.log(arr56?.[10]);

const fn56 = null;
console.log(fn56?.());

const api56 = {
  getName() { return 'Alice'; }
};

console.log(api56.getName?.());
console.log(api56.getAge?.());

const config56 = { port: 0, host: null, debug: false };

console.log(config56?.host ?? 'localhost');
console.log(config56?.port ?? 8080);
console.log(config56?.debug ?? true);

let user1_56;
console.log(user1_56?.profile?.name ?? 'Guest');

Visual: Short-Circuit Behavior

┌──────────────────────────────────────────────┐
│  user?.address?.city                         │
│                                              │
│  user = null                                 │
│    │                                         │
│    └──► user? → undefined                    │
│                   │                          │
│                   └──► STOP                  │
│                        address never read    │
│                        city never read       │
│                                              │
│  Result: undefined                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  user = { address: null }                    │
│                                              │
│  user? → user (not nullish)                  │
│    │                                         │
│    └──► address? → undefined                 │
│                       │                      │
│                       └──► STOP              │
│                            city never read   │
│                                              │
│  Result: undefined                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  user = { address: { city: 'Paris' } }       │
│                                              │
│  user? → user                                │
│    address? → address                        │
│      city → 'Paris'                          │
│                                              │
│  Result: 'Paris'                             │
│                                              │
└──────────────────────────────────────────────┘

Visual: ?. vs &&

┌──────────────────────────────────────────────┐
│  Old way (&&)                                │
│                                              │
│  user && user.address && user.address.city   │
│                                              │
│  Verbose. Repeats the path.                  │
│  Returns the falsy value, not undefined.     │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  New way (?.)                                │
│                                              │
│  user?.address?.city                         │
│                                              │
│  Concise. No repetition.                     │
│  Always returns undefined on short-circuit.  │
│                                              │
└──────────────────────────────────────────────┘

Visual: Three Forms

┌──────────────────────────────────────────────┐
│  Property:   obj?.prop                       │
│                                              │
│  Dynamic:    obj?.[key]                      │
│                                              │
│  Call:       fn?.()                          │
│                                              │
│  Same operator — different context           │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptSyntaxExample
Propertyobj?.propuser?.name
Dynamicobj?.[key]obj?.[key]
Callfn?.()cb?.()
Chaina?.b?.cAny level safe
With defaulta?.b ?? 'x'Fallback
Short-circuitStops at first null/undefined—
Falsy continues0, '', falseNot nullish
Assignment❌ not protectedobj?.p = x fails
delete✅ protecteddelete obj?.p
Compose with ??✅a?.b ?? c

Key takeaways:

  • ?. safely accesses properties, indices, and method calls
  • It short-circuits on null or undefined — returns undefined and skips the rest
  • Three forms: obj?.prop, obj?.[key], fn?.()
  • Only triggers on null and undefined — not on 0, '', or false
  • Does not protect assignments — only reads
  • delete obj?.prop is safe
  • Combine with ?? for defaults
  • Use it for API responses, optional config, optional callbacks, and optional methods
  • Don’t overuse — if a value should exist, fail fast
  • Truthiness checks after ?. still fail for falsy values — use != null
  • ?. isn’t a substitute for validation — it hides bugs when overused
  • Works with destructuring, method chaining, and array access

Remember: ?. is the safe navigation operator. Use it to gracefully handle data that might be missing — API responses, optional config, user input. It short-circuits on null and undefined — never on falsy values. Pair it with ?? for clean defaults. Don’t overuse it — sometimes an error is better than silence. Master ?., and nested access stops throwing.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!