?? and Optional Chaining ?.Safe Data Access in JavaScript
Nullish Coalescing (??) and Optional Chaining (?.) are the most important additions to JavaScript since ES2020. They replace fragile && chains and || fallbacks with precise, readable operators, with the crucial difference that they react only to null and undefined, not to every falsy value.
Table of Contents
- 1. The Problem with || as a Fallback Operator
- 2. Nullish Coalescing ?? in Detail
- 3. Optional Chaining ?. for Objects and Arrays
- 4. Optional Chaining for Function Calls
- 5. Combining ?? and ?.
- 6. Logical Assignment Operators: ??=, ||= and &&=
- 7. Practical Example: Safe API Response Processing
- 8. Common Pitfalls and Misconceptions
- 9. Operators in Direct Comparison
- 10. Summary
- 11. FAQ
1. The Problem with || as a Fallback Operator
Before ES2020, || was the usual way to define a fallback value: const timeout = config.timeout || 5000. This works for many cases, but it has a fundamental flaw: || uses JavaScript's "falsy" semantics. In JavaScript, not just null and undefined are falsy, but also 0, "" (empty string), false, and NaN. That means config.timeout || 5000 returns 5000 when config.timeout holds the value 0, even though 0 could be a valid, explicitly set timeout value. This is a classic, hard-to-find bug in JavaScript applications.
The same problem affects empty strings and false: options.label || 'Default' returns 'Default' when options.label === '', even though an explicitly empty string can be a valid, deliberately set value. Nullish Coalescing with ?? solves this problem by reacting only to null and undefined and treating every other value, including 0, '', and false, as valid. This is what fundamentally distinguishes ?? from || and makes it the right tool for default values in optional configuration parameters.
2. Nullish Coalescing ?? in Detail
The Nullish Coalescing operator ?? returns the left operand if it is not null or undefined, and the right operand otherwise. The semantics are exact: only the two "not present" values in JavaScript, null for explicitly set but empty, undefined for not set at all, trigger the fallback. Every other value, no matter how falsy it may otherwise appear, is passed through as a valid value. 0 ?? 'default' evaluates to 0. '' ?? 'default' evaluates to ''. false ?? true evaluates to false. Only null ?? 'default' and undefined ?? 'default' evaluate to 'default'.
The Nullish Coalescing operator short-circuits: the right operand is only evaluated when the left operand is null or undefined. This matters for side effects, if the right operand is a function call, it is not invoked when the left operand already has a valid value. ?? has lower precedence than most other operators, but it cannot be combined directly with || or && without parentheses, the parser throws a SyntaxError to prevent ambiguity.
// Nullish Coalescing ??, only null and undefined trigger the fallback
// The || problem: falsy values trigger fallback even when valid
const config = { timeout: 0, label: "", enabled: false, retries: null };
// WRONG: || treats 0, '', false as "missing"
const timeout1 = config.timeout || 5000; // 5000, WRONG, 0 is a valid timeout
const label1 = config.label || "Default"; // "Default", WRONG, "" may be intentional
const enabled1 = config.enabled || true; // true, WRONG, false is an explicit value
// RIGHT: ?? only triggers for null/undefined
const timeout2 = config.timeout ?? 5000; // 0, correct
const label2 = config.label ?? "Default"; // "", correct
const enabled2 = config.enabled ?? true; // false, correct
const retries = config.retries ?? 3; // 3, correct, null triggers fallback
// ?? is short-circuit: right side only evaluated when left is null/undefined
let callCount = 0;
const expensive = () => { callCount++; return "computed"; };
const result1 = "existing" ?? expensive(); // expensive() NOT called, callCount: 0
const result2 = null ?? expensive(); // expensive() IS called, callCount: 1
// ?? with chaining, first non-null/undefined wins
const value = config.primaryValue ?? config.fallbackValue ?? "hardcoded default";
// SyntaxError: cannot mix ?? with || or && without parentheses
// const x = a || b ?? c; // SyntaxError
const x = (a || b) ?? c; // OK, explicit precedence with parentheses
3. Optional Chaining ?. for Objects and Arrays
The Optional Chaining operator ?. allows safe access to properties of an object that might be null or undefined. Without ?., user.address.city results in a TypeError: Cannot read properties of undefined when user.address does not exist. The classic workaround was an && chain: user && user.address && user.address.city. With Optional Chaining, this simply becomes: user?.address?.city, which returns undefined if null or undefined occurs anywhere in the chain instead of throwing an error.
For arrays, Optional Chaining offers the ?.[index] operator: users?.[0]?.name returns undefined if users is null/undefined or the first element does not exist. This is especially useful for dynamic array access where the length of the array is not known in advance. An important aspect: Optional Chaining treats only null and undefined as an abort condition, exactly like ??. An object like { address: null } makes ?. in user?.address?.city stop at address and return undefined, while { address: {} } continues and returns undefined because city is not defined.
4. Optional Chaining for Function Calls
Optional Chaining works not only for property access, but also for function calls with the ?.() operator. callback?.(arg) calls callback only if it is not null or undefined, otherwise it returns undefined without throwing a TypeError. This is the idiomatic pattern for optional callback parameters: instead of if (callback) callback(data), you simply write callback?.(data).
The same applies to methods on objects: element?.focus?.() first checks whether element exists (element?.), and then whether it has a focus method (focus?.()). The second ?. is useful when the object exists but an optional method might not be implemented, for example with polymorphic objects. In practice, you encounter this with DOM APIs: element.scrollIntoView?.() calls scrollIntoView only if the browser supports it, without having to check typeof beforehand.
// Optional Chaining ?., safe access through null/undefined
// Object property access, nested chains
const apiResponse = {
data: {
user: {
profile: {
avatar: { url: "https://example.com/avatar.jpg" }
}
}
}
};
// Without optional chaining, verbose and error-prone
const avatarUrl1 =
apiResponse &&
apiResponse.data &&
apiResponse.data.user &&
apiResponse.data.user.profile &&
apiResponse.data.user.profile.avatar &&
apiResponse.data.user.profile.avatar.url;
// With optional chaining, clean and concise
const avatarUrl2 = apiResponse?.data?.user?.profile?.avatar?.url;
// Array element access
const users = null;
const firstUser = users?.[0]?.name; // undefined, no TypeError
// Function call with ?.()
function processWithCallback(data, callback) {
const result = transform(data);
callback?.(result); // only called if callback is not null/undefined
}
// Method call, also works for optional methods
const element = document.getElementById("modal");
element?.focus?.(); // focus if element exists and has focus()
element?.scrollIntoView?.({ behavior: "smooth" }); // browser feature detection
// Delete with optional chaining (less common but valid)
// delete obj?.property; // no error if obj is null/undefined
// Optional chaining in template literals
const name = user?.profile?.displayName ?? user?.username ?? "Anonymous";
console.log(`Hello, ${name}!`);
// Combining with nullish coalescing for defaults
const avatar = user?.profile?.avatar?.url ?? "/images/default-avatar.svg";
const count = user?.notifications?.unread ?? 0;
5. Combining ?? and ?.
Combining Nullish Coalescing and Optional Chaining is the core pattern for defensive data access in JavaScript. user?.address?.city ?? 'Unknown' reads as one expression: "return the city from the user's address if it exists, otherwise the string 'Unknown'". This combination is so common that it counts as a standard in modern TypeScript and JavaScript codebases.
An important point when combining them: ?. returns undefined when the chain aborts, never null. This matters when defining a fallback with ??: ?? reacts to both null and undefined, so the fallback is triggered in every case. The order of evaluation is intuitive: first the ?. chain is evaluated and returns either the value or undefined. Then the result is checked against ??, and if it is undefined, the fallback is used. No parentheses needed, ?. has higher precedence than ??.
6. Logical Assignment Operators: ??=, ||= and &&=
ES2021 introduced three Logical Assignment Operators that build on the same semantics as ??, ||, and &&. x ??= y assigns y only if x is null or undefined, the Nullish Assignment operator. x ||= y assigns when x is falsy. x &&= y assigns when x is truthy. These operators short-circuit: the right side is not evaluated when the condition does not apply, unlike x = x ?? y, where both sides are always evaluated.
The most common use of ??= is lazy initialization of object properties: cache[key] ??= computeExpensive(key) computes the value only if it is not already in the cache. Without ??=, the standard pattern was cache[key] = cache[key] ?? computeExpensive(key) or an explicit if. ??= makes this shorter and expresses the intent more clearly. This pattern also completely replaces the classic object.property || (object.property = default), which had || semantics and caused unwanted reassignments for falsy values.
// Logical Assignment Operators: ??=, ||=, &&=
// ??= (Nullish Assignment): assign only when null or undefined
let config = { timeout: 0, retries: null };
config.timeout ??= 5000; // 0, no assignment (0 is not null/undefined)
config.retries ??= 3; // 3, assigned (null triggers assignment)
config.maxSize ??= 100; // 100, assigned (undefined triggers assignment)
// ||= (Logical OR Assignment): assign when falsy
let opts = { label: "", count: 0 };
opts.label ??= "Default"; // "", no reassignment (??= is null/undefined only)
opts.label ||= "Default"; // "Default", reassigned (||= triggers on falsy "")
// &&= (Logical AND Assignment): assign when truthy
let user = { name: "Alice", role: "admin" };
user.role &&= user.role.toUpperCase(); // "ADMIN", assigned (role is truthy)
let guest = { name: "Bob" };
guest.role &&= guest.role.toUpperCase(); // undefined, no assignment (role is undefined)
// Lazy cache initialization with ??=
const cache = {};
function getCached(key) {
cache[key] ??= expensiveComputation(key); // computed only on first call
return cache[key];
}
// Equivalent without ??=, more verbose
function getCachedOld(key) {
if (cache[key] == null) { // null or undefined check
cache[key] = expensiveComputation(key);
}
return cache[key];
}
// Default object initialization with ??=
function initOptions(userOpts) {
userOpts ??= {};
userOpts.theme ??= "light";
userOpts.language ??= "en";
userOpts.timeout ??= 30_000;
return userOpts;
}
7. Practical Example: Safe API Response Processing
The most important practical use of Nullish Coalescing and Optional Chaining is processing API responses. REST APIs and GraphQL responses can omit fields for various reasons: optional fields, null as an explicit "no value" marker, nested objects that only exist for certain entity types, or partially loaded data. Without Optional Chaining and Nullish Coalescing, every access to a potentially missing field results in a TypeError or requires cumbersome guard conditions.
With Optional Chaining and Nullish Coalescing, API response processing becomes both shorter and more robust. The pattern response?.data?.items?.map(...) ?? [] returns an empty array if response, data, or items is null/undefined, instead of throwing a TypeError. response?.data?.pagination?.total ?? 0 returns a safe default number even if the pagination data is missing. This pattern is the foundation for defensive data transformations in every JavaScript application that communicates with external APIs.
8. Common Pitfalls and Misconceptions
Three common misconceptions about Nullish Coalescing and Optional Chaining. First: ?. and ?? react only to null and undefined, not to every falsy value. obj?.prop does not throw an error when obj is false, 0, or '', it would instead throw a TypeError, because false.prop, 0.prop, and ''.prop are technically valid expressions in JavaScript (primitives get autoboxed). ?. only guards against accessing null and undefined, not against other types.
Second: Optional Chaining is not allowed on the left side of an assignment. user?.address = city is a SyntaxError. Optional Chaining is for read access only, never for write access. Third: too much Optional Chaining can be a symptom of an architecture problem. If a chain like a?.b?.c?.d?.e?.f is required to read a value, that points to overly nested data structures that could be avoided through a better data model or early normalization of the data. Optional Chaining is a tool for genuine optionality, not for papering over poor data modeling.
9. Operators in Direct Comparison
The following table shows a direct comparison of the four related operators, ||, &&, ??, and ?., with their respective trigger and typical use.
| Operator | Trigger | Returns | Typical Use |
|---|---|---|---|
| ?? | null or undefined | right operand as fallback | Default values for optional configuration |
| || | all falsy values | right operand as fallback | Fallback when there is no valid truthy value |
| ?. | null or undefined in the access path | undefined (no TypeError) | Safe access to optional object properties |
| && | left operand is falsy | left operand (short-circuit) | Conditional expressions and guard patterns |
| ??= | left side is null or undefined | right operand (and assigns it) | Lazy init, cache population |
// Real-world API response processing with ?? and ?.
async function processUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`).catch(() => null);
// Safe extraction, no TypeError if response is null or structure differs
const user = response?.data?.user;
return {
// String defaults
name: user?.profile?.displayName ?? user?.username ?? "Anonymous",
bio: user?.profile?.bio ?? "",
location: user?.profile?.location ?? "Location unknown",
// Numeric defaults, ?? is critical here: 0 is a valid count
postCount: user?.stats?.posts ?? 0,
followerCount: user?.stats?.followers ?? 0,
rating: user?.stats?.avgRating ?? null, // null = "no rating yet"
// Boolean defaults, ?? not || (false is valid: user chose not to show email)
showEmail: user?.settings?.showEmail ?? true,
isVerified: user?.verification?.status === "verified",
// Array defaults, map only if items exist
tags: user?.tags?.map(t => t.name) ?? [],
badges: user?.achievements?.filter(a => a.visible)?.map(a => a.id) ?? [],
// Nested optional call, only call toISOString if date exists
lastActive: user?.activity?.lastSeen
? new Date(user.activity.lastSeen).toISOString()
: null,
};
}
Mironsoft
JavaScript development, code reviews, and defensive programming
Want to harden a codebase with fragile data access?
We analyze existing JavaScript codebases for fragile || fallbacks and unguarded object access, and migrate them to modern ?? and ?. patterns.
Code Review
Analysis of existing ||/&& patterns for falsy bugs and unsafe access
Modernization
Migration to ??, ?., ??=, ||=, and &&= with automated codemods
TypeScript
Enable strict null checks and leverage ?. within the TypeScript type system
10. Summary
Nullish Coalescing (??) and Optional Chaining (?.) are two of the most impactful additions to JavaScript since ES2020. The crucial difference from their predecessors: both operators react exclusively to null and undefined, not to every falsy value. This makes ?? the right tool for default values in optional configuration parameters, where 0, '', and false can be valid, explicitly set values that should not trigger a fallback. ?. makes deeply nested object access safe without && chains or explicit guard conditions.
The combination value?.deeply?.nested?.property ?? defaultValue is the idiomatic pattern for defensive data access in modern JavaScript. The Logical Assignment Operators ??=, ||=, and &&= round out the toolkit with short-circuiting assignments for lazy initialization and cache population. For TypeScript projects, Nullish Coalescing and Optional Chaining reinforce the strict null checks system and make potential null/undefined paths explicitly visible both to the compiler and to the reader.
Nullish Coalescing and Optional Chaining, the Essentials at a Glance
?? vs. ||
?? reacts only to null/undefined. || reacts to all falsy values (0, '', false, NaN). Always prefer ?? for default values in optional parameters.
Optional Chaining ?.
obj?.prop?.sub for objects. arr?.[0] for arrays. fn?.() for function calls. Returns undefined instead of TypeError when null/undefined occurs in the path.
Logical Assignment
x ??= y: assignment only when null/undefined. x ||= y: assignment when falsy. x &&= y: assignment when truthy. All short-circuit, the right side is not always evaluated.
Common Pitfalls
?. only guards against null/undefined, not other types. No assignment with ?. (SyntaxError). Do not mix ?? and || directly (SyntaxError), use parentheses.