JavaScript Optional Chaining (?.) and Nullish Coalescing (??)
Anyone who's worked with JavaScript for more than a day has written code like this:
Key Insights
- Optional chaining (
?.) eliminates verbose null checks by short-circuiting property access when encounteringnullorundefined, returningundefinedinstead of throwing errors - Nullish coalescing (
??) provides default values only fornullorundefined, unlike||which treats all falsy values (0, “”, false) as triggers for the default - Combining both operators creates cleaner API response handling and configuration patterns, reducing defensive code from dozens of lines to single expressions
The Problem with Nested Property Access
Anyone who’s worked with JavaScript for more than a day has written code like this:
let street;
if (user && user.address && user.address.street) {
street = user.address.street;
} else {
street = 'Unknown';
}
This pyramid of doom appears everywhere: API responses, configuration objects, user data structures. The traditional approach requires checking every level of nesting to avoid the dreaded TypeError: Cannot read property 'X' of undefined.
Here’s the same logic with modern JavaScript:
const street = user?.address?.street ?? 'Unknown';
One line. No nested ifs. No temporary variables. This isn’t just syntactic sugar—it’s a fundamental improvement in how we handle uncertain data structures.
Optional Chaining (?.) Explained
Optional chaining short-circuits property access when it encounters null or undefined. Instead of throwing an error, the expression immediately returns undefined and stops evaluating.
The syntax works with three access patterns:
Property Access:
const user = {
name: 'Alice',
address: {
city: 'Portland'
}
};
console.log(user?.address?.street); // undefined
console.log(user?.address?.city); // 'Portland'
console.log(user?.profile?.avatar); // undefined
When user.address.street doesn’t exist, we get undefined instead of an error. The chain stops at address when it realizes street is missing.
Array Indexing:
const users = [
{ name: 'Alice' },
{ name: 'Bob' }
];
console.log(users?.[0]?.name); // 'Alice'
console.log(users?.[5]?.name); // undefined
console.log(undefined?.[0]); // undefined
const maybeArray = null;
console.log(maybeArray?.[0]); // undefined (not an error!)
This is particularly useful when dealing with API responses where arrays might be missing or empty.
Function Calls:
const obj = {
regularMethod() {
return 'exists';
}
};
console.log(obj.regularMethod?.()); // 'exists'
console.log(obj.missingMethod?.()); // undefined
console.log(obj.optional?.().chain); // undefined
// Practical example: optional callbacks
function processData(data, onSuccess) {
const result = transform(data);
onSuccess?.(result); // Only calls if onSuccess exists
}
The ?.() syntax checks if the function exists before attempting to call it. This eliminates the typeof callback === 'function' checks scattered throughout callback-heavy code.
Nullish Coalescing (??) Explained
The nullish coalescing operator returns its right-hand operand when the left-hand operand is null or undefined. This seems similar to the logical OR (||), but the difference is critical:
// The || operator treats ALL falsy values as triggers
const count1 = 0 || 10; // 10 (wrong!)
const message1 = '' || 'Default'; // 'Default' (wrong!)
const flag1 = false || true; // true (wrong!)
// The ?? operator only triggers on null/undefined
const count2 = 0 ?? 10; // 0 (correct!)
const message2 = '' ?? 'Default'; // '' (correct!)
const flag2 = false ?? true; // false (correct!)
This distinction matters in real applications:
function createUser(options) {
return {
name: options.name ?? 'Anonymous',
age: options.age ?? 18,
notifications: options.notifications ?? true,
theme: options.theme ?? 'light'
};
}
// With || this would fail:
createUser({ name: '', age: 0, notifications: false });
// Using ||: { name: 'Anonymous', age: 18, notifications: true, theme: 'light' }
// All values replaced! User's choices ignored.
// With ?? this works correctly:
// Using ??: { name: '', age: 0, notifications: false, theme: 'light' }
// Only missing values get defaults.
The ?? operator respects intentional falsy values. Zero is a valid age. Empty strings are valid names. False is a valid boolean setting.
Combining Both Operators
The real power emerges when you combine optional chaining with nullish coalescing:
API Response Handling:
async function fetchUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return {
name: data?.user?.profile?.displayName ?? 'Anonymous User',
avatar: data?.user?.profile?.avatar ?? '/default-avatar.png',
bio: data?.user?.profile?.bio ?? '',
followerCount: data?.user?.stats?.followers ?? 0,
isVerified: data?.user?.verification?.status ?? false
};
}
Without these operators, this function would require 15+ lines of null checks. The intent is clear: access nested properties safely, provide sensible defaults.
Configuration Objects:
class DatabaseConnection {
constructor(config) {
this.host = config?.database?.host ?? 'localhost';
this.port = config?.database?.port ?? 5432;
this.maxConnections = config?.database?.pool?.max ?? 10;
this.timeout = config?.database?.timeout ?? 30000;
this.ssl = config?.database?.ssl?.enabled ?? false;
this.retries = config?.database?.retries ?? 3;
}
}
// Works with partial config
const db = new DatabaseConnection({
database: { host: 'prod-db.example.com' }
});
// Uses prod host, defaults for everything else
// Works with no config
const localDb = new DatabaseConnection({});
// All defaults
Form Data Processing:
function submitForm(formData) {
const payload = {
email: formData?.contact?.email ?? '',
phone: formData?.contact?.phone ?? null,
preferences: {
newsletter: formData?.preferences?.newsletter ?? false,
notifications: formData?.preferences?.notifications ?? true,
theme: formData?.preferences?.theme ?? 'auto'
},
metadata: {
source: formData?.metadata?.source ?? 'direct',
campaign: formData?.metadata?.campaign ?? null
}
};
return api.post('/submit', payload);
}
Browser Support and Best Practices
Both operators shipped in ES2020. Current support:
- Chrome 80+ (February 2020)
- Firefox 72+ (January 2020)
- Safari 13.1+ (March 2020)
- Node.js 14+
For older environments, use Babel:
// .babelrc
{
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": ["last 2 versions", "ie >= 11"]
}
}]
],
"plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator"
]
}
When NOT to use these operators:
Don’t use optional chaining when you expect the property to exist. It masks bugs:
// Bad: Hides missing required data
function processOrder(order) {
const total = order?.items?.reduce(...); // Should fail if items missing!
}
// Good: Fails fast on invalid data
function processOrder(order) {
if (!order.items) throw new Error('Order must have items');
const total = order.items.reduce(...);
}
Don’t chain excessively. If you’re writing a?.b?.c?.d?.e?.f, your data structure might be the problem.
Performance is negligible—these operators compile to simple checks. The real cost is in code that doesn’t fail when it should.
Conclusion
Optional chaining and nullish coalescing eliminate entire categories of defensive programming. They make code more readable by removing null-check noise and more correct by distinguishing between “missing” and “intentionally falsy.”
Adopt these operators when:
- Handling external data (APIs, user input, third-party libraries)
- Working with optional configuration
- Dealing with data that legitimately might not exist
The result is code that’s shorter, clearer, and less prone to the bugs that plague traditional null-checking approaches. These aren’t just conveniences—they’re fundamental improvements to how JavaScript handles the uncertainty inherent in real-world data.