Why optional chaining is making your JavaScript harder to debug
The optional chaining operator (?.) is one of the most convenient features in modern JavaScript. It's saved us countless lines of defensive null-checking code and made our codebases cleaner. But like any powerful tool, it can be misused—and when it is, it transforms from a helpful safeguard into a bug-hiding machine.
The Golden Rule
Use ?. only where it's actually okay for something not to exist.
This sounds simple, but in practice, I see developers (including past me) sprinkling ?. everywhere like it's syntactic sugar with no side effects. The truth is, every time you use optional chaining, you're making a statement about your data contract: "This might not exist, and that's fine."
A Real-World Example: User Profile Dashboard
Let's look at a common scenario in a user profile dashboard:
function renderUserProfile(user) {
return {
name: user?.name,
email: user?.email,
street: user?.address?.street,
city: user?.address?.city,
premium: user?.subscription?.isPremium
};
}
At first glance, this looks defensive and safe. But here's the problem: if user is undefined, you've got a serious bug. Your authentication failed, your data fetch failed, or your routing logic is broken. This function should never be called without a user object.
By using user?., you're silencing what should be a loud, immediate error. Instead, you'll get a profile full of undefined values, which might render as empty strings or cause subtle UI bugs that are much harder to trace back to the root cause.
The Better Approach
function renderUserProfile(user) {
return {
name: user.name,
email: user.email,
street: user.address?.street,
city: user.address?.city,
premium: user.subscription?.isPremium
};
}
Now, if user is undefined, you get an immediate error: Cannot read property 'name' of undefined. This is good. This tells you exactly where your data contract broke.
Meanwhile, address and subscription are genuinely optional—not all users have addresses on file, and not all users have subscriptions. Using ?. here is correct.
Example 2: API Response Handling
Here's another scenario I've seen go wrong in production:
async function getUserOrders(userId) {
const response = await fetch(`/api/users/${userId}/orders`);
const data = await response?.json();
return data?.orders?.map(order => ({
id: order?.id,
total: order?.total,
items: order?.items?.length
}));
}
This code is dangerously defensive. Let me show you what happens when the API is down:
-
fetch()might reject, but even if it resolves, the response might be an error status -
response?.json()will silently fail if response is null -
data?.orders?.map()will returnundefinedinstead of throwing - Each
order?.idhides the fact that your data structure is wrong
The result? Your error monitoring shows "undefined orders" instead of "API returned 500" or "Response parsing failed." You've turned a clear, debuggable error into a mystery.
The Better Approach
async function getUserOrders(userId) {
const response = await fetch(`/api/users/${userId}/orders`);
if (!response.ok) {
throw new Error(`Failed to fetch orders: ${response.status}`);
}
const data = await response.json();
// Orders array might be empty, but should always exist
return data.orders.map(order => ({
id: order.id,
total: order.total,
// Items array might be missing in draft orders
items: order.items?.length ?? 0
}));
}
Now your errors are explicit and actionable. The only optional chaining is on order.items, which genuinely might not exist for draft orders.
Example 3: Redux/State Management
This is where I see the most egregious overuse:
function selectUserDisplayName(state) {
return state?.user?.profile?.displayName ||
state?.user?.profile?.firstName ||
'Guest';
}
If state is undefined, your entire Redux store is broken. If state.user is undefined, your authentication slice isn't initialized. These are catastrophic errors that should crash immediately, not gracefully fall back to 'Guest'.
The Better Approach
function selectUserDisplayName(state) {
const profile = state.user.profile;
// displayName and firstName are optional fields
return profile.displayName ||
profile.firstName ||
'Guest';
}
If this crashes because state.user is undefined, that's exactly what should happen. You'll catch it immediately in development, not three weeks later when a user reports "weird login behavior."
The Mental Model: Required vs. Optional
When you're about to use ?., ask yourself:
"Is this data optional by design, or am I just being defensive?"
- Optional by design: User's phone number, middle name, company address, optional features
- Just being defensive: The user object itself, required configuration, authenticated session data
Here's a helpful rule of thumb:
// BAD: Being defensive about required data
const config = await loadConfig();
const apiUrl = config?.api?.baseUrl; // If config is undefined, you have bigger problems
// GOOD: Being defensive about optional data
const config = await loadConfig();
const apiUrl = config.api.baseUrl;
const optionalCacheUrl = config.api.cache?.url; // Cache is genuinely optional
What About TypeScript?
TypeScript makes this even more important. The optional chaining operator affects type narrowing:
interface User {
name: string;
address?: Address;
}
function printUserCity(user: User) {
console.log(user?.address?.city); // TypeScript happy, but wrong
console.log(user.address?.city); // Correct: user is required, address is optional
}
The second version correctly models your data contract and will help catch errors at compile time.
When Optional Chaining IS Perfect
Don't get me wrong—?. is fantastic when used correctly:
// API might return partial data
const userLocation = apiResponse.location?.coordinates?.lat;
// DOM element might not exist
document.querySelector('.modal')?.classList.remove('hidden');
// Callback might not be provided
options.onSuccess?.();
// Array might be empty
const firstResult = results[0]?.title;
// Object from external library might have optional properties
const theme = mui.theme?.palette?.primary.main;
In all these cases, the absence of data is expected and acceptable. Your code should handle it gracefully.
The Bottom Line
Every ?. you write is a tiny decision about whether something not existing is a bug or a feature. Make that decision consciously.
Stop using optional chaining as a safety blanket. Use it as documentation of your data contracts. When you look at code with appropriate use of ?., you can immediately see what's required and what's optional—and your bugs will scream at you instead of hiding in the shadows.
Your future self—the one debugging at 2 AM—will thank you.
Top comments (0)