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 RightOptional 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.