Beyond the Convenience - Rethinking Optional Chaining for Cleaner Code
Introduction
When optional chaining was introduced in JavaScript, it felt like a lifesaver. I used to write code like this:
const address = user && user.address ? user.address : '';
Honestly, I was terrible at writing clean code back then, and I have no shame admitting it. Then optional chaining came along and saved a lot of code space, making those checks much simpler:
const address = user?.address ?? '';
But after using optional chaining for a few years and growing as a developer, I started wondering: Is optional chaining really the best thing for writing clean, maintainable code?
What the Null Conditional Operator Does (and Why It Feels Magical)
I first came across optional chaining in C# back in 2018, when I was working as an intern. It felt like magic — finally, a way to get rid of endless null checks! This syntax isn’t unique to JavaScript, though; many modern languages have their own version of it like Kotlin and Dart.
It lets you safely access nested properties by quietly returning undefined
if anything along the chain is null or undefined — no crashes, no fuss.
Clean, concise, and elegant. But here’s the catch — it can also lull you into a false sense of safety.
The Illusion of Safety: A React Example
Let me show you a real scenario from React development. At first glance, this component looks perfectly fine:
import React from 'react';
function UserProfile({ user }) {
const address = user?.address ?? 'No address provided';
return (
<div>
<h2>User Profile</h2>
<p>Address: {address}</p>
<button onClick={() => alert(`Full address: ${address}`)}>Show Address Details</button>
</div>
);
}
Looks clean, right? No crashes, no errors in the console. But here’s the problem: what if user
itself is undefined
? The component silently renders “No address provided” — but should a user profile even render without a user?
This is where optional chaining can hide important bugs. A parent component might have failed to fetch user data, or passed props incorrectly, but this component just shrugs and displays fallback text. No warnings, no errors, just silent failure.
Optional Chaining: Before vs. After
Let’s revisit that React example with more intentional error handling:
Before: Silently hiding problems
function UserProfile({ user }) {
const address = user?.address ?? 'No address provided';
const name = user?.name ?? 'Unknown';
return (
<div>
<h2>{name}</h2>
<p>Address: {address}</p>
</div>
);
}
What’s happening here? Is missing user data expected in your application flow, or is something broken upstream?
After: Making expectations clear
function UserProfile({ user }) {
if (!user) {
throw new Error('UserProfile requires a user object');
}
const address = user.address ?? 'No address provided';
const name = user.name;
return (
<div>
<h2>{name}</h2>
<p>Address: {address}</p>
</div>
);
}
Now the code is explicit. If user
is missing, you’ll catch it immediately during development. You can then use React error boundaries to handle this gracefully in production, rather than silently rendering partial UI.
When Optional Chaining Can Lead You Astray
I don’t want to demonize ?.
—it’s a neat tool. But overusing it can mean you’re sweeping potential issues under the rug.
In one code review, I noticed how liberally ?.
was sprinkled everywhere. A key API response slipped through as undefined
, leading to mysterious crashes downstream that were a nightmare to debug.
Back to our React example: imagine if this component is part of a user dashboard. Without proper validation, a failed API call could render an entire page with “Unknown” names and “No address provided” everywhere. Users would see broken UI, but no error would be logged. Debugging this becomes painful because the failure point is hidden.
The operator doesn’t prevent null errors; it hides them silently. You end up wondering: are these nulls expected in the domain, or are we just ignoring errors because “the code doesn’t crash”?
Why This Matters for Maintainability
When you write explicit checks instead of hiding behind ?.
, you force yourself and your teammates to think about intent:
- Why might this property actually be null?
- Is it a valid case, or a hidden bug?
- Should we document or handle this differently?
In the React component, being explicit about required props makes the contract clear. Other developers know immediately: “This component needs a user object.” Optional chaining often encourages “just in case” coding — “let’s hedge our bets because I’m not sure.” That uncertainty can make the system brittle and harder to reason about as it grows.
When Null Should Be Treated as an Error
Let’s improve our React example further:
import React from 'react';
function requireNonNull(value, message) {
if (value === null || value === undefined) {
throw new Error(message);
}
return value;
}
function UserProfile({ user }) {
const validUser = requireNonNull(user, 'UserProfile: user prop is required');
// address CAN be null (optional field in the domain)
const address = validUser.address ?? 'No address provided';
return (
<div>
<h2>{validUser.name}</h2>
<p>Address: {address}</p>
</div>
);
}
This approach makes your contracts crystal clear. The component fails fast if user
is missing, but gracefully handles missing address
because that’s a valid domain scenario.
Failing fast means catching the mistake early, rather than silently passing nulls down the pipeline.
When Null Might Actually Be Valid
Of course, sometimes null means something important: “No data available yet,” or “Optional field.”
In our React example, address
being null is perfectly valid — not every user has an address on file. But user
being null? That’s a bug in how the component is being used.
This distinction is crucial:
type UserProps = {
user: User; // required - fail if missing
};
type User = {
name: string;
address: string | null; // optional - handle gracefully
};
This communicates to every caller: handle the possibility of absence intentionally.
Misusing Optional Chaining: A Real Example
Consider a factory method:
const fooBar = factory?.createFooBar();
If createFooBar()
never returns null, the ?.
is misleading. It suggests uncertainty that isn’t there.
Better to just write:
const fooBar = factory.createFooBar();
If null can be returned, be explicit and fail fast:
const fooBar = factory.createFooBar();
if (!fooBar) throw new Error('FooBar creation failed');
Clearer, safer, easier to maintain.
Design by Contract: The Better Way
The principle of design by contract means defining clear input-output conditions and enforcing them.
When done right:
- You catch errors immediately (fail fast).
- You use optional chaining only where null is truly expected.
- You document why null is acceptable, not just shrug at it.
For React components, this means proper prop validation and early guards. Pair this with error boundaries for production:
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
// Log to error reporting service
console.error('Component error:', error, errorInfo);
}
render() {
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<UserProfile user={currentUser} />
</ErrorBoundary>;
This leads to software that’s more predictable—and code easier to trust.
Should You Stop Using Optional Chaining?
Not at all. It’s a fantastic tool when used smartly.
Use it where null is part of the domain (like parsing external data or optional fields). Avoid it when core logic expects non-null values.
In our React example: use ?.
for address
(optional field), but not for user
(required prop).
Don’t treat ?.
like a safety net you throw in everywhere—treat it like a scalpel, wielded thoughtfully.
Final Thoughts
Optional chaining makes your code neat, but it doesn’t solve fundamental design issues. It can hide bugs better than it prevents them.
Be deliberate about nullability in your domain. When in doubt, fail fast. Make your intentions clear—whether in TypeScript types, React prop types, or explicit validation functions.