CodeChallenge LogoCodeChallenge

null Checks vs Optional Chaining vs the Maybe Pattern

Three strategies for handling missing values in JavaScript. When to use each, when they backfire, and why the safest option is not always the best.

July 20, 2026
8 min read
#javascript #null #optional-chaining #maybe-pattern #error-handling
null Checks vs Optional Chaining vs the Maybe Pattern

You write if (user && user.profile && user.profile.name) for the hundredth time and pause. There has to be a cleaner way, right? Optional chaining exists. The Maybe pattern exists. But each comes with trade-offs that aren't obvious until they bite you in production.

The problem is universal: values in your program might not exist. An API field might be missing. A config key might not be set. A user might not have completed their profile. How you handle this determines whether your code crashes gracefully, silently swallows bugs, or explodes at 3 AM with a Cannot read properties of null error.

There are three approaches to handle missing values. Each has a loyal following, and each has a dark side. Let's break them down.

Short version: Optional chaining is best for unreliable data (API responses). Manual checks are best for critical paths where you need to know exactly what failed. The Maybe pattern is best for data pipelines where a value passes through multiple transformations. Pick the tool that matches your data's reliability, not the one that looks shortest.

The Manual Check - Verbose but Honest

This is the original approach. You check every level explicitly before accessing the next:

function getCity(user) {
  if (user && user.address && user.address.city) {
    return user.address.city
  }
  return 'Unknown'
}

It is ugly. It is repetitive. But it is also completely transparent. Every developer reading this code knows exactly what can be null and what happens when it is. There is no hidden behavior, no magic, no silent fallback to undefined.

Where it shines is critical paths. Think payment processing, authentication checks, or database writes. In those places, you want the verbosity because each check documents an assumption your code is making. When the check fails, you know exactly which field was missing and at what depth.

The downside hits hard with deeply nested objects. A five-level check becomes an unreadable pyramid. This is not maintainable. If you find yourself writing nested checks beyond three levels, it is a sign that either your data structure is too deep or you need a different strategy.

Optional Chaining - Concise but Silent

Optional chaining (?.) was the JavaScript feature developers asked for years. It lets you write the nested access in a single line:

const city = user?.address?.city ?? 'Unknown'

Beautiful. Readable. One line versus six. No contest, right?

Not quite. The problem with optional chaining is that it returns undefined for any missing level. If user is null, you get undefined. If user.address is null, you get undefined. If user.address.city is an empty string, you get an empty string - which the nullish coalescing operator (??) will NOT catch because an empty string is not null or undefined.

This silent undefined is a double-edged sword. On one hand, it prevents crashes. On the other hand, it can hide the root cause of a bug. Consider this scenario: user was supposed to be loaded by the API but returned null. With optional chaining, profile?.displayName becomes undefined silently. No error. The UI shows nothing. The developer wonders why. With a manual check, this would throw Cannot read properties of null at the exact line where the problem is. Loud and clear.

Optional chaining is best for genuinely optional data - fields that might or might not exist by design. API responses are the classic example: the server might include profileImage only for users who uploaded one. In that case, optional chaining is exactly right because missing is a valid state, not a bug.

The Maybe Pattern - Safe but Verbose in a Different Way

The Maybe pattern wraps a value in a container that explicitly represents presence or absence. Instead of accessing properties directly, you transform the value through functions that handle both cases.

JavaScript does not have a native Maybe type, but the pattern is simple enough to implement:

function Maybe(value) {
  return {
    map: fn => value == null ? Maybe(null) : Maybe(fn(value)),
    unwrapOr: fallback => value == null ? fallback : value
  }
}

const city = Maybe(user)
  .map(u => u.address)
  .map(a => a.city)
  .unwrapOr('Unknown')

This looks more verbose than optional chaining. But there is a crucial difference: the Maybe pattern forces you to handle absence at every step. You cannot accidentally access a property on a null value because the .map() function checks for you. And you cannot forget to provide a default because .unwrapOr() requires it at the end.

Languages like Rust take this further with Option<T> and the compiler enforces handling. In JavaScript, the pattern is voluntary - which means it works only if your team agrees to use it consistently.

The real strength of Maybe shines in data pipelines. Imagine processing an API response through multiple transformations: parse the JSON, extract the user object, get the display name, capitalize it. Each step is independent and testable. If any step receives null, the entire pipeline short-circuits to a default value. You can easily add, remove, or reorder steps without touching null-checking logic.

Each of these strategies has a place in your toolbox. But none of them is perfect. The same trait that makes one strategy great in one context makes it dangerous in another. Here is where each one falls apart.

When Each Strategy Backfires

Strategy Backfires When Real-World Example
Manual check The object graph is deep (3+ levels) or the same check repeats across many functions A settings panel with nested config objects checked on every render
Optional chaining A value that should exist is missing and the error propagates silently A user profile page shows blank because the API returned null but optional chaining hid it
Maybe pattern The team is not familiar with functional patterns and introduces bugs in the wrapper itself A custom Maybe implementation has a bug in flatMap that corrupts data silently

The table above summarizes the failure modes, but a visual comparison makes the structural difference between the three strategies even clearer. The illustration below lays them out side by side so you can see at a glance how each one approaches the same nested access problem.

Three strategies comparison: Manual Check, Optional Chaining, and Maybe Pattern shown side by side with their characteristics

Notice how the Manual Check column breaks each level into an explicit condition, the Optional Chaining column collapses everything into a single expression, and the Maybe Pattern column chains transformations through a wrapper. The shape of the code tells you everything about when to reach for each approach.

How to Choose

Here is a simple rule of thumb based on the reliability of your data source:

  • API responses (unreliable by nature): Use optional chaining. Fields appear and disappear based on server logic. You expect some to be missing. Optional chaining and nullish coalescing handle this naturally.
  • Business logic (should be reliable): Use manual checks. If a value is null when it should not be, you want the loudest possible error. A crash with a stack trace is better than a silent undefined that reaches the UI.
  • Data transformation pipelines: Use the Maybe pattern. When a value flows through multiple steps (parse, validate, transform, display), the Maybe pattern keeps each step clean and testable.
  • Configuration objects: Optional chaining with nullish coalescing. Config values are genuinely optional - you provide a default when they are missing and move on.

When all else fails, a decision flowchart helps you pick the right strategy without second-guessing. The diagram below walks through the same logic we just covered - starting from the data source and branching toward the appropriate pattern.

Decision flowchart for choosing the right null-handling strategy based on data source and reliability

The key takeaway from this flowchart is the extra loop: even if your data comes from an API, you still need to ask whether null is a valid state. If it is not, fall back to a manual check. The flowchart catches the nuance that a simple rule of thumb misses.

The CodeChallenge Connection

The null-handling strategies covered in this article are not abstract theory - they appear directly in several CodeChallenge frontend challenges. Each challenge forces you to choose the right strategy based on the context.

Form Validation Fixer - Manual Checks in Practice

The Form Validation Fixer challenge presents a multi-step registration form where some fields are optional and others are required. If a required field is missing, the form must show a clear error message - not silently fail. This is the textbook case for manual null checks: you want the loudest possible signal when something expected is absent. Try solving it without looking at the solution first, then check how your null-handling strategy compares to the patterns above.

Age Calculator - Validating Before Computing

The Age Calculator challenge requires you to validate that day, month, and year inputs exist and are within valid ranges before computing the user's age. A missing or invalid field should produce a descriptive validation error, not a silent NaN or a crash. This is where the check-early, fail-loudly philosophy of manual checks shines.

Github User Searcher - The Maybe Pipeline in Action

The Github User Searcher challenge fetches a user profile from the GitHub API. The response contains many optional fields: bio, company, location, blog URL, and more. Processing this response through a Maybe-style pipeline - where each optional field is extracted, transformed, and given a fallback - keeps the code clean and testable. Notice how the chain of .map() calls mirrors the Maybe pattern we built earlier.

Keyboard Trap Form - A Different Kind of Null

The Keyboard Trap Form challenge is not about null values, but about a similar concept: focus state that might not exist where you expect it. The same mindset applies - check that the active element exists before manipulating it, use fallback element references, and never assume a DOM node is present.

Key Takeaway

There is no universal best strategy. Each exists because it solves a specific problem. Manual checks give you transparency. Optional chaining gives you conciseness. The Maybe pattern gives you composability. The skill is not knowing how to use all three - it is knowing which one your current context needs.

When in doubt, ask yourself: if this value is null, is that a valid state or a bug? Optional chaining for valid states. Manual checks for bugs. Maybe pattern for pipelines.

Share:
Found this helpful?

Related Articles