Tackling "cannot read property of undefined"
Few errors are as familiar as TypeError: Cannot read properties of undefined. It happens
whenever you reach into a nested structure that isn't fully present. ES2020 introduced two
operators that, together, make safe access and sensible defaults clean and concise: optional
chaining (?.) and nullish coalescing (??). Used well, they replace verbose guard
chains and subtle || bugs. Used carelessly, they can hide real problems. This guide covers
both, how they combine, and when not to use them.
Optional chaining basics
The ?. operator accesses a property only if the value before it is not null or
undefined. Otherwise the whole expression short-circuits to undefined instead of throwing.
const user = { profile: { name: 'Ada' } }
user.profile?.name // 'Ada'
user.account?.balance // undefined no error, even though account is missing
user.account.balance // TypeError without ?.
Instead of user && user.account && user.account.balance, you write user?.account?.balance.
The expression stops at the first nullish link and yields undefined.
Three forms of optional chaining
?. works for property access, dynamic (bracket) access, and method calls.
obj?.prop // property access
obj?.[key] // dynamic/computed access
obj?.method?.() // method call — only calls if method exists
arr?.[0] // array element
The method-call form is especially handy: obj.method?.() calls method only if it exists,
avoiding "x is not a function" errors when an optional callback may be absent.
Short-circuiting
When ?. short-circuits, it abandons the entire rest of the chain — subsequent accesses and
calls don't run at all.
const result = a?.b.c.d
// if `a` is null/undefined, b, c, AND d are all skipped -> result is undefined
This is important: you only need ?. at the link that might be nullish, not on every step,
because short-circuiting protects the rest. That said, putting ?. at each genuinely-optional
link documents your assumptions clearly.
Nullish coalescing basics
The ?? operator returns its right-hand side only when the left is null or
undefined. It's the safe way to provide defaults.
const port = config.port ?? 8080 // use 8080 only if port is null/undefined
const name = user.name ?? 'Anonymous'
It pairs naturally with optional chaining to access-and-default in one expression:
const city = user?.address?.city ?? 'Unknown' // safe access + fallback
The crucial ?? vs || distinction
This is the heart of why ?? exists. The old || operator returns the right side for any
falsy value — including 0, '', false, and NaN — which causes bugs when those are
valid values.
const count = data.count || 10 // if count is 0, you get 10!
const count2 = data.count ?? 10 // 0 is kept; only null/undefined -> 10
const label = input.text || 'N/A' // empty string becomes 'N/A'
const label2 = input.text ?? 'N/A' // '' is a valid value, kept
Rule of thumb: use ?? when 0, '', or false are legitimate values you want to keep,
and || only when you genuinely want any falsy value to trigger the fallback.
Logical assignment operators
ES2021 added logical assignment operators that combine a logical operator with assignment:
??=, ||=, and &&=. They assign only when the condition holds.
config.timeout ??= 3000 // assign 3000 only if timeout is null/undefined
options.list ||= [] // assign [] only if list is falsy
user.active &&= validate() // assign validate() only if active is truthy
??= is the safest for defaults — it sets a value only when the property is genuinely missing,
preserving 0/''/false. These operators also short-circuit: the right side isn't
evaluated when the assignment is skipped.
Combining ?. and ?? with precedence
?. and ?? work together, but there's a syntax rule: you cannot directly mix ?? with
|| or && without parentheses, because their precedence is intentionally ambiguous.
const x = a ?? b || c // SyntaxError
const y = (a ?? b) || c // explicit grouping required
const z = a ?? (b || c) // also fine
This forced clarity prevents subtle bugs from mixed-operator expressions.
Don't over-use optional chaining
The biggest pitfall is reaching for ?. everywhere, which hides bugs. If a value should
always exist, ?. silently turns a programming error into a quiet undefined that surfaces as
a confusing failure later.
// if `user` should always exist, this masks the real bug
const name = user?.name
// let it throw loudly so you catch the actual problem
const name = user.name
Use ?. only where a value is legitimately optional (data that may or may not be present),
not as a blanket shield over code you haven't reasoned about. Over-chaining makes failures
harder to diagnose.
Optional chaining is for reading, not writing
You can't use ?. on the left side of an assignment — it's a read-only access operator.
obj?.prop = 5 // SyntaxError — invalid assignment target
if (obj) obj.prop = 5 // guard the write explicitly
For conditional writes, guard with a normal check or use ??=/||= on the property.
Key takeaways
?.accesses a property/method only if the preceding value isn'tnull/undefined, short-circuiting toundefinedinstead of throwing.- It comes in property (
?.prop), dynamic (?.[key]), and call (?.()) forms, and abandons the rest of the chain on a nullish link. ??returns the fallback only fornull/undefined— unlike||, it preserves0,'', andfalse.- Logical assignment (
??=,||=,&&=) assigns conditionally and short-circuits;??=is the safe default-setter. - You must parenthesize when mixing
??with||/&&. - Don't over-use
?.— it can mask real bugs; reserve it for genuinely optional values, and remember it can't be an assignment target.
Optional chaining and nullish coalescing make defensive code clean and intention-revealing — as long as you respect the falsy-vs-nullish distinction and don't paper over errors that should surface.