Optional Chaining & Nullish Coalescing Interview Questions & Answers

30 questions Updated 2026-06-18

JavaScript optional chaining and nullish coalescing interview questions — the ?. operator on properties, methods, and calls, ?? vs ||, logical assignment operators ??= ||= &&=, short-circuiting, and precedence pitfalls.

Read the in-depth guideOptional Chaining & Nullish Coalescing in JavaScript — Safe Access Done Right

Optional chaining (?.) accesses a nested property only if the value to its left is not null or undefined; otherwise the whole expression short-circuits and returns undefined instead of throwing.

const user = { profile: { name: 'Ada' } }
console.log(user.profile?.name)      // 'Ada'
console.log(user.account?.balance)   // undefined  no throw
// console.log(user.account.balance) // TypeError: Cannot read 'balance' of undefined

It replaces long a && a.b && a.b.c guards. Pitfall: it guards against null/ undefined only — it does not protect against other errors like calling a non-function.

a?.b evaluates a; if it's null/undefined it returns undefined, otherwise it accesses b. Chaining several ?. guards each link in the path.

const data = { a: { b: null } }
console.log(data?.a?.b?.c)           // undefined  stops safely at b being null

Importantly, ?. only checks the value immediately to its left. Pitfall: data.a?.b.c guards a but not b — if b is null, accessing .c still throws. Put ?. at every uncertain link.

Use obj.method?.(). If method is null or undefined, the call is skipped and the expression yields undefined; otherwise the method runs normally.

const api = { log: () => 'logged' }
console.log(api.log?.())             // 'logged'
console.log(api.track?.())           // undefined  no track method, no throw

Handy for optional callbacks: onChange?.(value). Pitfall: if method exists but is not a function (e.g. a number), ?.() still throws TypeError: not a function?. only guards null/undefined, not wrong types.

Use arr?.[index]. The ?. before the bracket guards the array itself being null/undefined before the element is read.

const result = { items: ['a', 'b'] }
console.log(result.items?.[0])       // 'a'
console.log(result.tags?.[0])        // undefined  no tags array, no throw

Same applies to computed keys: obj?.[key]. Pitfall: arr?.[0] guards arr, not the element — if arr is [], arr?.[0] is undefined (fine), but arr?.[0].name throws because element 0 is undefined.

When ?. hits a null/undefined, it stops evaluating the rest of the chain and returns undefined immediately — nothing to the right runs, including function calls.

let calls = 0
const obj = null
const r = obj?.method(calls++)       // undefined
console.log(calls)                   // 0  method() and calls++ never ran

The entire chain after the short-circuit is skipped, side effects included. Pitfall: people assume arguments still evaluate — they don't, so relying on a side effect inside a short-circuited call is a silent bug.

a ?? b returns a unless it is null or undefined, in which case it returns b. It provides a default only for "no value", not for any falsy value.

const port = userPort ?? 3000        // use userPort unless it's null/undefined
console.log(0 ?? 5)                  // 0  0 is a real value, kept
console.log(null ?? 5)               // 5  null -> fallback

It treats 0, '', false, and NaN as legitimate values. Pitfall: don't reach for ?? when you actually want to reject all falsy values — that's ||.

|| falls back on any falsy value (0, '', false, NaN, null, undefined). ?? falls back only on null/undefined. The difference matters whenever a falsy value is valid.

const count = 0
console.log(count || 10)             // 10  treats valid 0 as "missing"
console.log(count ?? 10)             // 0   keeps the real 0
console.log('' || 'default')         // 'default'  empty string lost
console.log('' ?? 'default')         // ''  empty string kept

Use ?? for defaults of numbers/strings/booleans. Pitfall: blindly migrating every || to ?? (or vice-versa) changes behavior for zero/empty inputs — a common subtle regression.

Use || when you genuinely want any falsy value to trigger the fallback — e.g. treating empty string, 0, or false the same as "absent."

const name = input.name || 'Anonymous'   // '' should also become Anonymous
const items = response.list || []        // falsy/empty -> safe default array

Here an empty string genuinely means "no name," so || is correct and ?? would let '' through. Pitfall: the choice is semantic — ask "is a falsy value a legitimate value here?" If yes use ??; if no, || is fine.

a ??= b assigns b to a only if a is currently null or undefined. It's shorthand for a = a ?? b, but with a key twist: it assigns only when needed.

const config = { timeout: 0 }
config.timeout ??= 1000              // stays 0  (0 isn't nullish)
config.retries ??= 3                 // becomes 3  (was undefined)
console.log(config)                  // { timeout: 0, retries: 3 }

It only writes when the left side is nullish, so existing valid values (even falsy ones) survive. Pitfall: confusing it with ||=, which would overwrite the 0.

a ||= b assigns b when a is falsy (a = a || b); a ??= b assigns only when a is nullish. The split mirrors the || vs ?? distinction.

let x = 0
x ||= 5                              // x = 5  overwrote valid 0
let y = 0
y ??= 5                              // y = 0  kept valid 0

Both short-circuit: if no assignment is needed, the right side never evaluates (no side effects, no setter triggered). Pitfall: using ||= to set a default on a numeric/boolean field clobbers legitimate 0/false.

a &&= b assigns b to a only if a is truthy (a = a && b). It's used to update a value only when it already exists.

let user = { name: 'Ada' }
user.name &&= user.name.toUpperCase()
console.log(user.name)               // 'ADA'  updated because it was truthy
let empty = null
empty &&= 'x'                        // stays null  falsy, left alone

Like the others it short-circuits — b only evaluates when a is truthy. Pitfall: it's the least common of the three; people often reach for an if when &&= would read more cleanly (or vice versa where if is clearer).

This is the canonical pair: ?. safely walks a possibly-missing path and yields undefined, then ?? supplies a default for that undefined.

const user = { settings: null }
const theme = user?.settings?.theme ?? 'light'
console.log(theme)                   // 'light'  safe walk + default

?. handles the "path might not exist," ?? handles the "so use a fallback." Pitfall: using || instead of ?? here would also override a legitimate theme of '' or 0, which ?? correctly preserves.

JavaScript forbids combining ?? with || or && without parentheses — it's a syntax error by design, because the intended precedence is ambiguous to readers.

// const x = a || b ?? c             // SyntaxError
const x = (a || b) ?? c              // explicit grouping required
const y = a || (b ?? c)              // the other grouping

The language forces you to disambiguate rather than guessing. Pitfall: this is a compile-time error, not a runtime one — copying a clever one-liner from &&/|| land into a ?? expression won't even parse.

No. ?. is only valid for reading (and deleting). Using it as an assignment target is a syntax error — you can't conditionally write through it.

const obj = { a: {} }
// obj?.a?.b = 5                     // SyntaxError — invalid assignment target
if (obj?.a) obj.a.b = 5             // guard, then assign normally

delete obj?.a.b is allowed (short-circuits to a no-op if obj is nullish), but assignment is not. Pitfall: people expect symmetric read/write support and are surprised the parser rejects the assignment form outright.

Sprinkling ?. everywhere hides real bugs. If a value should never be null, guarding it with ?. silently swallows the case where it unexpectedly is, turning a loud crash into a quiet undefined that surfaces far away.

// a required user that's accidentally null:
const name = user?.profile?.name    // becomes undefined, no signal something broke
const name2 = user.profile.name     // throws loudly at the actual fault line

Use ?. only where absence is legitimate and expected, not as a blanket crash-suppressor. Pitfall: defensive ?. everywhere defers the error to a confusing downstream location, making bugs far harder to trace.

Both default on undefined, but a destructuring default does not trigger on null, whereas ?? does. So they diverge whenever a value is explicitly null.

const { x = 1 } = { x: null }
console.log(x)                       // null  <- destructuring default ignores null
const y = ({ x: null }).x ?? 1
console.log(y)                       // 1     <- ?? catches null too

Use a destructuring default for "missing key," and ?? (or both) when null is also a "use the fallback" signal. Pitfall: assuming { x = d } and x ?? d behave identically — they differ precisely on null.

Chain ?. before the method call: arr?.map(...). If arr is null/ undefined, the whole call short-circuits to undefined.

const data = {}
const upper = data.tags?.map(t => t.toUpperCase())
console.log(upper)                   // undefined  no throw on missing tags
const safe = data.tags?.map(t => t) ?? []  // fall back to empty array

Pair with ?? [] if downstream code expects an array. Pitfall: data.tags?.map returning undefined will break a chained .filter() after it — guard the whole chain or supply the ?? [] fallback.

No — ?? short-circuits. The right operand evaluates only when the left is null/undefined. So an expensive default or a function call on the right is skipped when the left has a value.

let computed = 0
const v = 'cached' ?? expensive()    // expensive() never runs
function expensive() { computed++; return 'x' }
console.log(computed)                // 0  skipped

This makes value ?? buildDefault() cheap in the common case. Pitfall: relying on the right side's side effects is a bug, since it may never execute.

Once any ?. short-circuits, the entire remaining chain — further property accesses, calls, and index lookups — is skipped, and the whole expression is undefined. The short-circuit "infects" everything to its right.

const obj = { a: null }
console.log(obj.a?.b.c.d())          // undefined  b.c.d() all skipped

You don't need ?. on every link after the one that may be nullish — the first short-circuit covers the rest. Pitfall: but each independent uncertain link still needs its own ?.; the short-circuit only protects links downstream of where it fired.

Because 0 is a valid number but falsy, || would wrongly replace it. ?? only replaces null/undefined, so a real 0 survives.

const volume = settings.volume ?? 100   // volume of 0 stays 0
// const bad = settings.volume || 100   // 0 becomes 100 — muted unmute!

Same reasoning applies to any field where 0, '', or false are meaningful. Pitfall: a "default to 100" written with || silently breaks the legitimate-zero case — a classic slider/quantity bug.

Yes. obj?.method() keeps this bound to obj, exactly like a normal method call — the ?. only adds the nullish guard, it doesn't change the call's receiver.

const counter = {
  n: 5,
  get() { return this.n }
}
console.log(counter?.get())          // 5  this is still counter

The short-circuit happens before the call, so this is irrelevant when it fires. Pitfall: extracting the method first (const g = counter?.get; g()) loses this, just like without ?. — optional chaining doesn't bind methods.

a ?? b ?? c returns the first non-nullish value left to right. It's a clean way to express a fallback priority list.

const value = userSetting ?? cookieSetting ?? defaultSetting ?? 'fallback'
console.log(null ?? undefined ?? 0)  // 0  first non-nullish wins (0 counts)

Each ?? short-circuits, so evaluation stops at the first real value. Pitfall: this is valid (chaining ?? with ?? is fine), but mixing in a || mid-chain without parentheses is a syntax error.

Yes — delete obj?.prop deletes the property if obj exists, and is a no-op (returning true) if obj is null/undefined. It's one of the few non-read contexts ?. allows.

const obj = { a: 1 }
delete obj?.a                        // deletes a
const nothing = null
delete nothing?.x                    // no-op, no throw

Assignment is still forbidden, but deletion is permitted because it short-circuits cleanly. Pitfall: it's an uncommon form, so reviewers may not realize the delete is conditionally skipped when the object is nullish.

Because ??= only assigns (and only evaluates the right side) when the property is nullish, it's perfect for "compute once and cache" — subsequent calls skip the work.

const cache = {}
function get(key) {
  return cache[key] ??= expensiveLookup(key)  // computed once per key
}

On the first call cache[key] is undefined, so the lookup runs and stores; later calls return the cached value without recomputing. Pitfall: if expensiveLookup can legitimately return null/undefined, it's treated as "not cached" and re-runs every time.

When the chain succeeds, ?. returns the real value — it does not convert anything. It only injects undefined when it actually short-circuits on a nullish link.

const obj = { a: 0, b: false }
console.log(obj?.a)                  // 0      real value
console.log(obj?.b)                  // false  real value, not undefined
console.log(obj?.missing)            // undefined  (short-circuit)

So a result of undefined could mean "short-circuited" or "property is genuinely undefined." Pitfall: you can't distinguish those two cases from the result alone — use in or hasOwnProperty if the difference matters.

In templating/JSX, falsy-but-valid values like 0 or '' should render. || would replace them with the fallback, hiding real data; ?? keeps them.

// showing a like count:
const display = post.likes ?? 'N/A'  // 0 renders as 0
// const bad = post.likes || 'N/A'   // 0 likes shows 'N/A'

Same for displaying an empty search query or a false toggle label. Pitfall: || in a render path is a frequent source of "why does zero show as N/A?" bug reports.

Negligibly. ?. compiles to a simple null/undefined check before each access — a tiny conditional. It's far cheaper than the bugs and verbose guards it replaces.

// these are essentially equivalent in cost:
const a = obj && obj.prop
const b = obj?.prop                  // same check, cleaner

The "cost" worth watching is semantic, not CPU — readability and bug-hiding. Pitfall: don't avoid ?. for imagined perf reasons; the real concern is using it where a value should never be nullish (see over-use).

Combine ?.() (skip the call if the function is absent) with ?? (default the result if the call was skipped or returned nullish).

function process(data, transform) {
  return transform?.(data) ?? data   // use transform if given, else raw data
}
process(5)                           // 5  — no transform, falls back
process(5, x => x * 2)               // 10 — transform applied

transform?.(data) is undefined when no callback is passed, and ?? data supplies the fallback. Pitfall: if transform legitimately returns 0/'', ?? keeps it (good), but || data would wrongly discard those.

They're equivalent in result, but ??= is shorter and, crucially, short-circuits the assignment: when config.x already has a value, no write happens — avoiding spurious setter calls or proxy traps.

// verbose:
config.x = config.x ?? compute()
// concise + skips the write when x exists:
config.x ??= compute()               //

For plain objects the difference is cosmetic; for objects with setters or reactive frameworks it avoids unnecessary side effects. Pitfall: on a getter-only (read-only) property, ??= still attempts the write when nullish and throws in strict mode.

You can mix ?.[key] and ?.() freely in a single chain; each ?. guards its own link, and the first nullish one short-circuits the rest.

const handlers = { onSave: () => 'saved' }
const name = 'onSave'
console.log(handlers?.[name]?.())    // 'saved'
console.log(handlers?.['onDelete']?.())  // undefined  missing handler, no throw

This pattern safely dispatches to a dynamically-named handler that may not exist. Pitfall: handlers[name]?.() (no ?. before [name]) still throws if handlers itself is nullish — guard the root too.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.