DEV Community

Samuel Ochaba
Samuel Ochaba

Posted on

Stop Using `?.` Everywhere - You're Hiding Your Bugs

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
  };
}
Enter fullscreen mode Exit fullscreen mode

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
  };
}
Enter fullscreen mode Exit fullscreen mode

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
  }));
}
Enter fullscreen mode Exit fullscreen mode

This code is dangerously defensive. Let me show you what happens when the API is down:

  1. fetch() might reject, but even if it resolves, the response might be an error status
  2. response?.json() will silently fail if response is null
  3. data?.orders?.map() will return undefined instead of throwing
  4. Each order?.id hides 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
  }));
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)