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.
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
nullandundefined(a value that is missing). - Falsy = a bigger group that also includes
0,"",falseandNaN(values that are present but empty or zero).
Rule of thumb: use
??when0,""orfalseare 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 when0,""orfalseare 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
?.likeuser?.name = "x". That is a SyntaxError. - Expecting
?.to replace other checks. It only reacts tonullandundefined. A value like0or""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?.xstill throws a ReferenceError ifmissingwas never declared. - Forgetting that the result of
?.can beundefined. Handle it with??or anifbefore using it. - Using
?.()on something that is not a function. Ifuser.greetis the text"hi", thenuser.greet?.()throws a TypeError.?.()checks only fornullorundefined.
Practice
- Create
const user = { name: "Riya" };and printuser.address.city(observe the error), then fix it with?.. - Read
user.address?.cityand give it a default of"Unknown"using??. - Create
const settings = { volume: 0 };and show the difference betweensettings.volume || 50andsettings.volume ?? 50. - Write a function
getFirstItem(list)that returns the first item or"Empty"if the list is empty or missing. - Write a function
greet(user)that prints"Hello, <name>", using"Guest"when the name isnullorundefined. - Use
?.()to call an optionalonClickfunction that may not exist. - Use
??=to add default values forthemeandlanguageto an options object, without changing values that already exist. - Create an object where
nicknameis"". Show the result with||and with??, and explain which one you would use in a real app. - 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
?.returnsundefinedinstead of throwing an error when the value before it isnullorundefined. - It has three forms:
obj?.prop,obj?.[key]andfn?.(). - 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 isnullorundefined. - Nullish means only
nullandundefined. Falsy also includes0,"",falseandNaN. - Use
??when0,""orfalseare 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
MapandSet, two special collections for storing key-value pairs and unique values.