JavaScriptIntermediate

JavaScript Optional Chaining (?.) and Nullish Coalescing (??)

Learn optional chaining and nullish coalescing in JavaScript to read nested data safely, set defaults correctly, and avoid the || and falsy value trap.

All JavaScript lessons

What you will learn

Real data is rarely perfect. A user may not have an address, an API may not send a phone number, a button may not exist on the page. If you try to read a property of undefined, JavaScript throws an error and your program stops. In this lesson you will learn two modern operators that solve this problem: optional chaining (?.), which reads nested data safely, and nullish coalescing (??), which gives a default value only when a value is really missing. You will also learn how ?? is different from ||, and why that difference matters.

The problem: reading from nothing

const user = { name: "Riya" };

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

user.address is undefined, and you cannot read .city from undefined. The old fix was to check every level:

const city = user && user.address && user.address.city;

This works, but it is long and ugly. Optional chaining makes it short.

Optional chaining: ?.

Put ?. where a value might be missing. If the value before ?. is null or undefined, JavaScript stops and returns undefined instead of throwing an error.

const user = { name: "Riya" };

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

With a value that exists, it works like a normal dot:

const user = { name: "Riya", address: { city: "Delhi" } };

console.log(user.address?.city);   // "Delhi"

Going deeper

You can use it at several levels:

const user = { profile: { social: { twitter: "@riya" } } };

console.log(user.profile?.social?.twitter);     // "@riya"
console.log(user.profile?.social?.instagram);   // undefined
console.log(user.account?.billing?.plan);       // undefined (stops at account)

Three forms of optional chaining

Form Use for Example
obj?.prop A property user?.name
obj?.[key] Bracket access, array items list?.[0], obj?.["first-name"]
fn?.() A function or method that may not exist user.greet?.()
const data = { items: ["pen", "book"] };

console.log(data.items?.[0]);        // "pen"
console.log(data.tags?.[0]);         // undefined

const user = { name: "Riya" };

console.log(user.greet?.());         // undefined (no greet method, no error)

Optional chaining on a method is very handy for optional callbacks:

function loadData(onDone) {
  // ... do some work ...
  onDone?.("finished");   // call it only if onDone was provided
}

loadData();                              // fine, nothing happens
loadData((msg) => console.log(msg));     // "finished"

It short-circuits

When the value before ?. is missing, the rest of the chain is skipped, including function calls:

let count = 0;
const user = null;

user?.update(count++);

console.log(count);   // 0 (count++ never ran)

It only checks null and undefined

?. does not react to other falsy values like 0 or "":

const score = 0;
console.log(score?.toFixed(1));   // "0.0" (0 is not null, so it continues)

const text = "";
console.log(text?.length);        // 0

Optional chaining cannot be used for assignment

const user = {};

// user?.name = "Riya";   // SyntaxError: Invalid left-hand side in assignment

You can only read with ?.. To write, check first with an if.

Nullish coalescing: ??

?? gives you a default value, but only when the left side is null or undefined (we call these nullish values).

const name = null;
console.log(name ?? "Guest");   // "Guest"

const age = undefined;
console.log(age ?? 18);         // 18

const city = "Delhi";
console.log(city ?? "Unknown"); // "Delhi"

In words: “use the left value, unless it is missing, then use the right value.”

?? vs ||: the important difference

Before ??, people used || for defaults. But || reacts to every falsy value, not only missing ones. Falsy values are false, 0, "" (empty text), null, undefined, NaN and 0n.

const volume = 0;

console.log(volume || 50);   // 50  (wrong! 0 is a real volume)
console.log(volume ?? 50);   // 0   (correct)

This table shows what happens:

Left value value || "default" value ?? "default"
undefined "default" "default"
null "default" "default"
0 "default" 0
"" "default" ""
false "default" false
NaN "default" NaN
"hello" "hello" "hello"

Nullish vs falsy: the simple rule

  • Nullish = only null and undefined (a value that is missing).
  • Falsy = a bigger group that also includes 0, "", false and NaN (values that are present but empty or zero).

Rule of thumb: use ?? when 0, "" or false are valid values. Use || only when you really want to replace every falsy value.

const settings = { darkMode: false, retries: 0, nickname: "" };

console.log(settings.darkMode || true);   // true  (wrong: user chose false)
console.log(settings.darkMode ?? true);   // false (correct)

console.log(settings.retries ?? 3);       // 0
console.log(settings.nickname ?? "Guest");   // ""

A case where || is the better choice: an empty name should also become “Guest”:

const input = "";
console.log(input || "Guest");   // "Guest" (empty text is not useful here)
console.log(input ?? "Guest");   // ""

Using them together

Optional chaining returns undefined when something is missing, and ?? turns that into a default. They are a perfect pair:

const user = { name: "Riya" };

const city = user.address?.city ?? "City not set";

console.log(city);   // "City not set"
const response = { data: { items: [] } };

const first = response.data?.items?.[0] ?? "No items";
console.log(first);   // "No items"

Nullish assignment: ??=

a ??= b means “assign b to a only if a is null or undefined”.

let theme;
theme ??= "light";
console.log(theme);   // "light"

let language = "hi";
language ??= "en";
console.log(language);   // "hi" (already has a value)

It is a short form of a = a ?? b. There are also two cousins:

let a = 0;
a ||= 10;    // assigns if a is falsy
console.log(a);   // 10

let b = 5;
b &&= 20;    // assigns if b is truthy
console.log(b);   // 20

Use ??= for “fill in missing defaults” without touching 0, "" or false:

const options = { size: 0 };

options.size ??= 10;        // stays 0
options.color ??= "blue";   // added

console.log(options);   // { size: 0, color: "blue" }

Mixing ?? with && or ||

JavaScript does not allow ?? to be mixed with || or && without parentheses. This protects you from confusing results:

// const x = a || b ?? c;    // SyntaxError

const a = null, b = 0, c = "default";

const x = (a || b) ?? c;     // OK: (null || 0) is 0, then 0 ?? "default" is 0
console.log(x);              // 0

Real-life use cases

1. Safe API data

const apiResponse = {
  user: { name: "Riya", contact: { email: "riya@example.com" } }
};

const email = apiResponse.user?.contact?.email ?? "No email";
const phone = apiResponse.user?.contact?.phone ?? "No phone";

console.log(email);   // "riya@example.com"
console.log(phone);   // "No phone"

2. Elements that may not exist on the page

const title = document.querySelector("#title")?.textContent ?? "No title found";
console.log(title);

// Safely add a class only if the element exists
document.querySelector("#box")?.classList.add("active");

If #box does not exist, querySelector returns null, and ?. stops quietly instead of crashing.

3. Settings with valid zero and false values

function createPlayer(options = {}) {
  return {
    volume: options.volume ?? 50,
    muted: options.muted ?? false,
    speed: options.speed ?? 1
  };
}

console.log(createPlayer({ volume: 0 }));
// { volume: 0, muted: false, speed: 1 }   (volume 0 is respected)

4. Optional callbacks

function saveForm(data, { onSuccess, onError } = {}) {
  const ok = Boolean(data.name);

  if (ok) {
    onSuccess?.("Saved!");
  } else {
    onError?.("Name is required");
  }
}

saveForm({ name: "Riya" }, { onSuccess: console.log });   // "Saved!"
saveForm({ name: "" });                                    // nothing happens, no error

5. Reading an item from a list that may be empty

const cart = [];

const first = cart[0]?.name ?? "Cart is empty";
console.log(first);   // "Cart is empty"

6. A user display name with fallbacks

const user = { nickname: null, firstName: "Riya" };

const display = user.nickname ?? user.firstName ?? "Guest";
console.log(display);   // "Riya"

You can chain ?? to try several values from left to right.

7. Optional methods on values that may be missing

const value = null;

console.log(value?.toString());      // undefined
console.log(value?.toFixed(2));      // undefined

const price = 49.5;
console.log(price?.toFixed(2));      // "49.50"

8. Reading query parameters

const params = new URLSearchParams("?page=0&sort=price");

const page = Number(params.get("page") ?? 1);
const limit = Number(params.get("limit") ?? 10);

console.log(page);    // 0   (page 0 is kept)
console.log(limit);   // 10  (limit was missing)

Common mistakes

  • Using || for defaults when 0, "" or false are valid. Use ??.
  • Using ?. everywhere “just in case”. It can hide real bugs. Use it only where the value is truly optional. If a value must exist, let the error show.
  • Trying to assign with ?. like user?.name = "x". That is a SyntaxError.
  • Expecting ?. to replace other checks. It only reacts to null and undefined. A value like 0 or "" is not skipped, so ?. will keep going and use it.
  • Forgetting parentheses when mixing ?? with || or &&. It is a SyntaxError.
  • Expecting ?. to catch an undeclared variable. missing?.x still throws a ReferenceError if missing was never declared.
  • Forgetting that the result of ?. can be undefined. Handle it with ?? or an if before using it.
  • Using ?.() on something that is not a function. If user.greet is the text "hi", then user.greet?.() throws a TypeError. ?.() checks only for null or undefined.

Practice

  1. Create const user = { name: "Riya" }; and print user.address.city (observe the error), then fix it with ?..
  2. Read user.address?.city and give it a default of "Unknown" using ??.
  3. Create const settings = { volume: 0 }; and show the difference between settings.volume || 50 and settings.volume ?? 50.
  4. Write a function getFirstItem(list) that returns the first item or "Empty" if the list is empty or missing.
  5. Write a function greet(user) that prints "Hello, <name>", using "Guest" when the name is null or undefined.
  6. Use ?.() to call an optional onClick function that may not exist.
  7. Use ??= to add default values for theme and language to an options object, without changing values that already exist.
  8. Create an object where nickname is "". Show the result with || and with ??, and explain which one you would use in a real app.
  9. Challenge: write getValue(obj, "a.b.c", defaultValue) that safely reads a nested value from a text path, using ?. or a loop and ??.

Recap

  • Optional chaining ?. returns undefined instead of throwing an error when the value before it is null or undefined.
  • It has three forms: obj?.prop, obj?.[key] and fn?.().
  • It short-circuits: the rest of the chain is skipped when something is missing. It works only for reading, not assignment.
  • Nullish coalescing ?? returns the right side only when the left side is null or undefined.
  • Nullish means only null and undefined. Falsy also includes 0, "", false and NaN.
  • Use ?? when 0, "" or false are valid values. Use || when every falsy value should be replaced.
  • ??= assigns a value only when the variable is nullish. Mixing ?? with || or && needs parentheses.
  • ?. and ?? together are the standard way to read optional data with a default: user.address?.city ?? "Unknown".
  • Next you will learn Map and Set, two special collections for storing key-value pairs and unique values.