Opening: A simpler way to handle nulls
Before optional chaining and nullish coalescing landed in JavaScript, I spent half my time writing defensive code to avoid Cannot read property of null errors. Two operators changed that for me: ?. and ??. Learning ?. and ?? done right removed roughly half my null checks and made my code breathe easier. Let me show you exactly how.
The problem: null and undefined everywhere
Every real-world API response, user object, or configuration file can have missing fields. Before optional chaining, you'd chain guards like this:
// Old way: nested guards everywhere
const city = user && user.address && user.address.city;
const country = user.address ? user.address.country : undefined;
if (config && config.database && config.database.host) {
connect(config.database.host);
}
This works, but it's verbose and the logic gets buried. You're forced to think about the shape of the object at every step. On production platforms I've built, this pattern appeared hundreds of times—once in user validation, once in API response parsing, once in component rendering. It's repetitive and easy to miss a guard, leaving bugs.
Optional chaining (?.) returns undefined instead of throwing
Optional chaining lets you safely access nested properties. If any step in the chain is null or undefined, the whole expression short-circuits and returns undefined—no error thrown.
// With optional chaining
const city = user?.address?.city;
const country = user?.address?.country;
// Works on arrays and function calls too
const firstItem = items?.[0];
const result = callback?.();
// Combine them all
const nested = user?.profile?.settings?.theme?.color;
I use this constantly in React components. When a prop might not exist or a data fetch hasn't completed yet, optional chaining keeps the render safe:
// In a React component
function UserCard({ user }) {
return (
<div>
<h1>{user?.name ?? 'Anonymous'}</h1>
<p>Email: {user?.email}</p>
<img src={user?.avatar?.url} alt="avatar" />
<button onClick={user?.profile?.onEdit}>Edit</button>
</div>
);
}
No crash if user is null. No crash if avatar doesn't exist. The component gracefully handles incomplete data.
Nullish coalescing (??) only falls back on null or undefined
This is where many developers get tripped up. The ?? operator is not the same as ||. It only treats null and undefined as "missing"—everything else, including 0, empty strings, false, and empty arrays, is kept.
// ?? only falls back on null or undefined
const timeout = config.timeout ?? 5000; // 0 → uses 0, not 5000
const enabled = settings.enabled ?? true; // false → uses false, not true
const message = response.message ?? 'OK'; // '' → uses '', not 'OK'
// || falls back on any falsy value
const timeout2 = config.timeout || 5000; // 0 → uses 5000 (wrong!)
const enabled2 = settings.enabled || true; // false → uses true (wrong!)
const message2 = response.message || 'OK'; // '' → uses 'OK' (wrong!)
In a trading platform I worked on, this distinction mattered. A user's fee might legitimately be 0 (no fee, promotional account). Using || would incorrectly default to a fallback fee. Using ?? kept the zero:
// Trading platform: fee can be 0
const userFee = account.fee ?? 0.05; // Correct: 0 stays as 0
const userFee2 = account.fee || 0.05; // Wrong: 0 becomes 0.05
// API response: sometimes status is explicitly false
const isActive = user.isActive ?? true; // Correct: false stays as false
const isActive2 = user.isActive || true; // Wrong: false becomes true
On ride-hailing platforms, a driver's rating might be 0 when brand new. Status fields might be false. These are real values, not missing data. ?? respects that.
Combining ?. and ?? for clean defaults
The real power appears when you chain them together. You safely navigate nested structures with ?. and provide clean defaults with ??:
// E-commerce: product might not have variant prices
const price = product?.variants?.[0]?.price ?? 0;
// SaaS settings: user might not have configured notifications
const emailNotifications = user?.preferences?.notifications?.email ?? true;
// CRM: contact might not have a phone number
const phone = contact?.details?.phone?.number ?? 'Not provided';
// Next.js data fetching
const title = data?.seo?.title ?? data?.name ?? 'Untitled';
This pattern appears in almost every platform I've built. You're saying: "Try to access this nested value. If it doesn't exist anywhere, use this sensible default." No guards, no ternary nesting, clean and readable.
Real-world example: API response parsing
Let me show this in context. A REST API returns user data that might be incomplete or shaped differently across versions:
interface ApiUserResponse {
id: string;
name?: string;
profile?: {
avatar?: {
url?: string;
};
bio?: string;
};
settings?: {
emailNotifications?: boolean;
darkMode?: boolean;
};
}
function parseUser(response: ApiUserResponse) {
return {
id: response.id,
name: response.name ?? 'Anonymous',
avatarUrl: response.profile?.avatar?.url ?? '/default-avatar.png',
bio: response.profile?.bio ?? '',
emailNotifications: response.settings?.emailNotifications ?? true,
darkMode: response.settings?.darkMode ?? false,
};
}
// Works even if most fields are missing
const user1 = parseUser({ id: '123' });
// { id: '123', name: 'Anonymous', avatarUrl: '/default-avatar.png', bio: '', emailNotifications: true, darkMode: false }
const user2 = parseUser({
id: '456',
name: 'Alice',
profile: { avatar: { url: 'https://...' } },
settings: { darkMode: true },
});
// { id: '456', name: 'Alice', avatarUrl: 'https://...', bio: '', emailNotifications: true, darkMode: true }
No try-catch blocks. No manual null checks. No crashes. The data flows through cleanly.
Common mistakes with optional chaining
Chaining too far and hiding bugs
This is the trap I fell into early. If you chain optional chaining across too many levels, you lose track of where the data actually comes from:
// Too much chaining—bugs hide here
const value = response?.data?.user?.profile?.settings?.preferences?.notifications?.email?.address;
// Later: why is value always undefined?
// You don't know if response is null, or if settings is null, or if notifications is null.
// You've lost visibility.
// Better: break it into logical steps
const userData = response?.data?.user;
const settings = userData?.profile?.settings;
const notifications = settings?.preferences?.notifications;
const emailAddr = notifications?.email?.address ?? 'not-set@example.com';
Breaking it apart helps debugging. You can log each step. You can see exactly where the chain breaks if data is missing.
Mixing ?? with || incorrectly
Developers sometimes use || where they should use ??, especially when migrating old code:
// Wrong: || treats 0 and false as missing
const retryCount = config?.retries || 3; // 0 becomes 3
const isEnabled = feature?.enabled || false; // false stays false (works by accident)
// Correct: ?? only checks null/undefined
const retryCount = config?.retries ?? 3; // 0 stays 0
const isEnabled = feature?.enabled ?? false; // false stays false
Audit your codebase for this pattern, especially in config handling and feature flags.
Assuming ?? always runs
Remember that ?? only runs if the left side is truly null or undefined. An empty string '' is not missing:
// If message is an empty string, it's preserved
const msg = user.message ?? 'No message'; // '' → stays ''
// If you want to treat empty string as missing, use || or check explicitly
const msg2 = user.message || 'No message'; // '' → becomes 'No message'
const msg3 = user.message?.trim() ? user.message : 'No message'; // '' → 'No message'
This is intentional, but it catches people off guard when they expect empty strings to be replaced.
When to use ?. vs when to check explicitly
Optional chaining is great for defensive coding, but sometimes you need to know whether a field is actually missing versus present but falsy:
// When you don't care about the difference
const city = user?.address?.city; // Could be undefined, null, or missing
// When you need to know for sure
const hasCity = user && user.address && user.address.city !== undefined;
const city = user?.address?.city;
if (city) {
// city is definitely present and truthy
}
// In an API: distinguish between "field not sent" and "field is null"
if ('city' in (user?.address || {})) {
// Field was sent (even if value is null)
} else {
// Field was not sent
}
Most of the time, optional chaining is the right move. But in strict validation or serialization, you might need finer control.
Performance and bundling
Optional chaining and nullish coalescing compile down to straightforward checks in your transpiled output. They're not slow or heavy—they're just syntax sugar for readable code:
// TypeScript source
const value = obj?.prop?.nested ?? 'default';
// Compiled JavaScript
var _obj$prop;
const value = (_obj$prop = obj === null || obj === void 0 ? void 0 : obj.prop.nested) !== null && _obj$prop !== void 0 ? _obj$prop : 'default';
No performance penalty. The transpiler handles it. Use these operators freely.
Key takeaways
- Optional chaining (?.) safely accesses nested properties—if any step is null or undefined, the expression returns undefined without throwing.
- Nullish coalescing (??) only treats null and undefined as "missing"—0, false, and empty strings are preserved, unlike || which treats them as falsy.
- Combine ?. and ?? for clean defaults—
obj?.nested?.value ?? fallbackis readable and safe. - Don't chain ?. too far—break long chains into logical steps so debugging is easier and bugs don't hide.
- Audit your codebase for || where ?? should be used—especially in config, feature flags, and numeric fields where zero or false are valid values.
- These are just syntax sugar—they compile to plain JavaScript with no performance cost, so use them confidently.
Final thought
In five years building production platforms, I've learned that small wins in readability compound. Optional chaining and nullish coalescing removed the tedious guard chains that cluttered my code. They let me focus on business logic instead of null checks. Use them, combine them, and your code—and your reviewers—will thank you.






Comments
Loading comments…