Knowledge Base/concepts/Optional Chaining and Nullish Coalescing in JavaScript

Optional Chaining and Nullish Coalescing in JavaScript

Optional chaining and nullish coalescing are not a general permission slip to ignore missing data. They express a narrow rule: this path may be absent, and only null or undefined should use the fallback. That distinction is why user?.profile?.city ?? "Unknown" can be safer than both a crashing property read and a broad || fallback.

The operators are small. The judgment around them is the real lesson.

Contents

The short version

Use optional chaining, ?., when an object path may stop before the property you want:

const city = user?.profile?.address?.city;

If user, profile, or address is null or undefined, the expression returns undefined instead of throwing a TypeError.

Use nullish coalescing, ??, when you want a fallback only for null or undefined:

const city = user?.profile?.address?.city ?? "Unknown";

That expression keeps real values such as "", 0, and false. It falls back only when the path is actually missing.

The Optional Chaining exercise practices exactly this shape: read user.profile.address.city safely, then return "Unknown" when any part of the path is missing.

Optional chaining stops a missing path

A normal nested property read assumes every step exists:

const user = null;

console.log(user.profile.address.city);
// TypeError: Cannot read properties of null

That error is useful when user is required. It tells you the caller broke the contract. But if user is allowed to be missing, the crash is just noise.

Optional chaining checks the value immediately before ?.:

const user = null;

console.log(user?.profile?.address?.city); // undefined

The chain stops as soon as it reaches null or undefined. It does not stop for other falsy values:

const settings = {
  theme: {
    name: "",
  },
};

console.log(settings?.theme?.name); // ""

That empty string is still a value. Optional chaining does not treat it as missing.

For the missing-value background, see null vs undefined in JavaScript.

Read nested objects one uncertain step at a time

Optional chaining is most useful when data crosses a boundary: API responses, saved settings, optional profile fields, feature configuration, browser APIs, or user-generated content.

const response = {
  user: {
    profile: {
      address: {
        city: "Lisbon",
      },
    },
  },
};

const city = response.user?.profile?.address?.city;

console.log(city); // "Lisbon"

The important part is where the ?. appears. It protects only the read that comes directly after it.

This is incomplete if profile might be missing:

const user = {};

console.log(user?.profile.address?.city);
// TypeError: Cannot read properties of undefined

user?.profile is safe, but .address is a normal property read on whatever profile returns. If profile is undefined, the next step still crashes.

Protect each uncertain step:

const user = {};

console.log(user?.profile?.address?.city); // undefined

Do not add ?. mechanically to every dot. Add it at the places where the data model says "this part may be absent."

Use optional calls carefully

Optional chaining can also call a function only when the function exists:

function saveDraft(draft, hooks = {}) {
  hooks.onBeforeSave?.(draft);

  const saved = {
    ...draft,
    savedAt: new Date().toISOString(),
  };

  hooks.onAfterSave?.(saved);

  return saved;
}

If onBeforeSave is missing, nothing happens. If it exists, it gets called.

That is useful for optional callbacks. It is not a type check.

const hooks = {
  onAfterSave: "send email",
};

hooks.onAfterSave?.({});
// TypeError: hooks.onAfterSave is not a function

Optional calls skip only null and undefined. If a value exists but is not callable, JavaScript still throws. When a value comes from an unknown source, check it with typeof first:

if (typeof hooks.onAfterSave === "function") {
  hooks.onAfterSave(saved);
}

The typeof guide covers where that check helps and where it is too broad.

Use nullish coalescing for safe fallbacks

Optional chaining often pairs with nullish coalescing:

function getCity(user) {
  return user?.profile?.address?.city ?? "Unknown";
}

console.log(getCity({ profile: { address: { city: "Lisbon" } } }));
// "Lisbon"

console.log(getCity({ profile: {} }));
// "Unknown"

console.log(getCity(null));
// "Unknown"

The left side tries to read the nested value. The right side supplies a fallback only when the result is null or undefined.

That means these values survive:

const product = {
  inventory: {
    count: 0,
  },
  flags: {
    archived: false,
  },
  display: {
    subtitle: "",
  },
};

console.log(product.inventory?.count ?? 10); // 0
console.log(product.flags?.archived ?? true); // false
console.log(product.display?.subtitle ?? "No subtitle"); // ""

Those are not missing values. They are real data, even if they are falsy.

Why nullish coalescing is different from OR

The || operator falls back when the left side is falsy. That includes 0, "", false, null, undefined, and NaN.

const savedVolume = 0;

const volumeWithOr = savedVolume || 50;
const volumeWithNullish = savedVolume ?? 50;

console.log(volumeWithOr); // 50
console.log(volumeWithNullish); // 0

If 0 means "muted," || ignored the user's setting. ?? kept it.

Here is the same distinction across common values:

const values = [0, "", false, null, undefined];

for (const value of values) {
  console.log({
    value,
    orFallback: value || "fallback",
    nullishFallback: value ?? "fallback",
  });
}

The first three values use the fallback with ||, but not with ??. Only null and undefined fall back with ??.

Use || when any falsy value should mean "use the default." Use ?? when only missing values should mean "use the default."

For the full coercion rule behind ||, read Truthy and Falsy Values in JavaScript. For short-circuit behavior, read JavaScript Logical Operators.

Grouping mistakes break the chain

Optional chaining protects one continuous chain. Parentheses can end that chain earlier than you think.

This is safe:

const user = null;

console.log(user?.profile?.address); // undefined

This is not:

const user = null;

console.log((user?.profile).address);
// TypeError: Cannot read properties of undefined

The grouped expression (user?.profile) evaluates to undefined. After that, .address is a normal property read, so it throws.

Keep the chain continuous:

const address = user?.profile?.address;

Or name the intermediate value and guard it clearly:

const profile = user?.profile;

if (profile === undefined) {
  return "No profile";
}

return profile.address;

Grouping also matters when you mix ?? with || or &&. JavaScript requires parentheses because otherwise the intent is easy to misread:

const label = cachedName ?? savedName || "Guest";
// SyntaxError

Choose the grouping that matches the rule:

const label = (cachedName ?? savedName) || "Guest";
const strictLabel = cachedName ?? (savedName || "Guest");

Those expressions are not equivalent. The first one allows the final || to replace an empty string. The second one keeps cachedName if it is anything other than null or undefined.

When grouping changes the business rule, prefer a few named variables over a dense one-liner.

Overuse can hide broken assumptions

Optional chaining is for optional data. It is not a good way to avoid deciding what your program requires.

This code may be too quiet:

function renderAccount(user) {
  return `${user?.profile?.name ?? "Unknown"} (${user?.role ?? "guest"})`;
}

If renderAccount should never receive a missing user, this function now hides the bug and renders fake data. The caller can be broken for weeks while the UI politely says "Unknown".

Make required data required:

function renderAccount(user) {
  if (user == null) {
    throw new Error("renderAccount requires a user");
  }

  const name = user.profile?.name ?? "Unknown";
  const role = user.role ?? "guest";

  return `${name} (${role})`;
}

Now the top-level user contract is loud, while genuinely optional profile fields still get safe fallbacks.

The same applies to DOM code:

document.querySelector("#save")?.addEventListener("click", save);

That is fine when the save button appears only on some pages. If the current page cannot work without it, use a guard that reports the missing element instead of silently skipping the event listener.

Optional chaining also cannot assign into a missing path:

user?.profile?.name = "Ada";
// SyntaxError

If you need to create missing objects, do that explicitly:

if (user.profile == null) {
  user.profile = {};
}

user.profile.name = "Ada";

That code says something stronger than an optional read. It says the object should now have a profile.

Debugging checklist

When optional chaining or nullish coalescing shows up in a bug, ask these in order:

  1. Is this value genuinely optional, or did a required contract break earlier?
  2. Which exact step may be null or undefined?
  3. Should 0, false, or "" be preserved?
  4. Would a clear error be better than a fallback?
  5. Is the expression too dense to show the rule plainly?

Here is a reasonable shape for mixed required and optional data:

function summarizeInvoice(invoice) {
  if (invoice == null) {
    throw new Error("summarizeInvoice requires an invoice");
  }

  const customerName = invoice.customer?.name ?? "Unknown customer";
  const total = invoice.totalCents ?? 0;
  const note = invoice.metadata?.publicNote ?? "";

  return {
    customerName,
    total,
    note,
  };
}

The function refuses to run without an invoice. Inside a real invoice, some fields may still be optional. That is the line you want to draw.

Key Takeaways

  • Use ?. when a specific object path may stop at null or undefined.
  • Put ?. on every uncertain step, not every property access by habit.
  • Use ?? when only null and undefined should trigger the fallback.
  • Do not replace ?? with || when 0, false, or "" are valid values.
  • Optional chaining should make optional data clear, not hide broken required data.
Share this article:
javascriptobjectsconditionalsdebugging