What you'll be able to do

By the end of this page you should be able to read a nested value like a city on a user object without the page dying when the user is missing, and pick a default with ?? so a real 0 still counts. You will also know when ?. is hiding a bug instead of fixing one.

Previous post: truthy, falsy, and boolean coercion.

Who this is for

  • People who saw Cannot read properties of null (or undefined) after writing user.profile.city
  • Beginners who used count || 10 and watched a real zero turn into ten
  • Anyone who sprinkled ?. on every line because a tutorial said it was “safer”

You can skip this if you already use ?. only on values that are allowed to be missing, and you default numbers with ?? instead of ||. Come back when a nested read crashes, or a fallback eats 0.

The crash: reading a property that is not there

JavaScript will not invent a nested object for you. If user is null or undefined, user.profile throws.

const user = null;
console.log(user.profile.city);

That is not a quiet undefined. It is an error, and the rest of the script stops.

Scenario A — data that sometimes exists. A logged-out page has no user. A logged-in page has user.profile.city. The same line of code runs in both cases.

Scenario B — a nested piece is missing. user exists, but profile was never set. user.profile.city still throws, because you asked for .city on undefined.

The old long way is a chain of checks:

if (user && user.profile && user.profile.city) {
  console.log(user.profile.city);
}

That works. It is also noisy, and it mixes “exists” with “is truthy” — so a city named in a weird empty way can still confuse you. Optional chaining is the short, honest version of “stop if this part is nullish.”

?. means “if this is null or undefined, stop and give undefined”

nullish here means null or undefined — not 0, not "", not false.

const missing = null;
const guest = { profile: { city: "Pune" } };

console.log(missing?.profile?.city);
console.log(guest?.profile?.city);

The first log is undefined (no crash). The second is "Pune".

Scenario A — a label on a page. const city = user?.profile?.city; then show city or a placeholder. The page stays up when user is missing.

Scenario B — a method that might not exist. user.getName?.() calls the function only if getName is there. If you write user.getName() and getName is missing, you crash. Optional chaining on a call is ?.() — the ?. sits before the parentheses.

You can also use it on an index: items?.[0]. Same idea: if items is null, you get undefined instead of a throw.

?. does not fix a wrong name. user?.proflie?.city (typo) is still undefined, quietly. That is the danger: the crash went away, and so did the clue. Use ?. on values that are allowed to be missing, not on every property because you are unsure how to spell it.

You cannot assign through optional chaining. user?.profile.city = "Pune" is not a thing to lean on — if user is missing, there is nowhere to write. Set up the object first, then assign.

?? means “use the right side only if the left is null or undefined”

This is nullish coalescing. It is the default operator that does not treat 0 or "" as missing.

console.log(0 || 10);
console.log(0 ?? 10);
console.log("" || "Guest");
console.log("" ?? "Guest");
console.log(null ?? "Guest");

That is 10, 0, "Guest", "", "Guest". || uses truthiness (last post). ?? only cares about null and undefined.

Scenario A — a count that can be zero. const shown = count ?? 10; keeps 0. count || 10 would replace it. If you are showing “clips left,” wiping zero is a product bug, not a style choice.

Scenario B — a name that can be empty on purpose. If an empty string should stay empty, name ?? "Guest" keeps "". If empty should become Guest, name || "Guest" (or trim first, then decide) is the better question. Pick the operator that matches the question, not the one you memorized first.

You will often pair them:

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

If the nested read stops early, you get undefined, then ?? supplies the label. If the city is "Pune", you keep it. If someone stored city: "", ?? keeps the empty string — trim or check length if empty should also fall back.

There is a shorthand assign: count ??= 10 means “if count is null or undefined, set it to 10.” It will not overwrite 0.

When not to reach for ?.

Optional chaining is not a substitute for knowing your data.

Scenario A — a value that must exist. If settings is required after login, settings?.theme hiding a missing object will show a blank theme and you will debug CSS. Throw or guard once at the top: if (!settings) { ... return; }.

Scenario B — a typo. ?. turns “I misspelled profile” into undefined. Log the object once. Spell the path. Then add ?. only on the optional steps.

Do not mix ?? and || in one expression without parentheses. The parser will complain or you will misread it. Write two lines, or wrap: (count ?? 0) || fallback only if you truly want both behaviors — most beginners do not.

A tiny practice file

Save as optional.html. The script pretends the user is missing, then present. Watch the paragraph: no crash, and zero stays zero.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Optional</title>
  </head>
  <body>
    <button id="missing">User missing</button>
    <button id="zero">User with 0 clips</button>
    <p id="out">Result will show here.</p>
    <script>
      const out = document.getElementById("out");

      function show(user) {
        const city = user?.profile?.city ?? "No city";
        const clips = user?.clips ?? 10;
        const clipsOr = user?.clips || 10;
        out.textContent =
          "City: " +
          city +
          ". clips ?? 10: " +
          clips +
          ". clips || 10: " +
          clipsOr;
      }

      document.getElementById("missing").addEventListener("click", function () {
        show(null);
      });

      document.getElementById("zero").addEventListener("click", function () {
        show({ profile: { city: "Pune" }, clips: 0 });
      });
    </script>
  </body>
</html>

Missing user: city becomes "No city", both clip defaults become 10. User with clips: 0: ?? shows 0, || shows 10. That click is the whole || vs ?? lesson.

Mistakes I see a lot

1. user.profile.city with no guard. One missing layer throws. Use user?.profile?.city.

2. count || 10 for a number that can be zero. Use count ?? 10.

3. ?. on every property “just in case.” You hide typos. Optional only the optional parts.

4. Calling a maybe-missing function as user.getName?. without (). That does not call it. You want user.getName?.().

5. Expecting ?. to create objects. It only reads. It will not build profile for you.

**6. Using ?? when you wanted empty string to fall back.** Empty is not nullish. Trim, then ||, or check length.

What to try before the next post

  1. In the console, set user = null and compare user.profile with user?.profile.
  2. Log 0 || 10 and 0 ?? 10.
  3. Log "" || "Guest" and "" ?? "Guest".
  4. Build the tiny HTML file and click both buttons.

This closes Phase 2 of the series (values). Next we begin control flow: if, else, and switch — choosing a path in a form or a UI without nesting yourself into a corner.

Try this next outside the series

Values become easier when you see them inside real forms and payloads, not only tiny console lines.