Nullish coalescing & optional chaining
Handle missing values with ?? and ?. instead of long checks, while preserving useful values like 0, false, and the empty string.
- 01Choose the right backupUse ?? when only null and undefined should use the backup value. Know when || is still the right choice.
- 02Read data that might be missingUse ?. before properties, square brackets, and calls. Know which errors it does not stop.
- 03Fill in or update a valueUse ??=, ||=, and &&=, and know the check that each one makes.
Missing is not the same as empty
In the Logical operators lesson, you saw a problem with count || 10. If count is 0, you get 10, and the real 0 is lost. This lesson shows a tool made for that problem. It is called nullish coalescing, and you write it as ??.
A value is nullish when it is null or undefined. Those are the only two nullish values. Other falsy values are not nullish, and they can be real data:
0can be a volume that is turned all the way down.falsecan be a checkbox that someone left unticked on purpose."", the empty string, can be a blank label that someone chose.
Use ?? to pick a backup value only when the first value is null or undefined. Use ?. to read a property that might not be there. If the value before ?. is null or undefined, you get undefined instead of an error.
?? is a backup planYou are planning a trip, and you keep a backup plan too. You use the backup only when you have no plan at all. A plan that says "walk zero blocks" is still a plan. A plan that says "don't send a reminder" is still a plan.
- In real life: You have no plan at all
- In JavaScript:
nullorundefined: use the backup - In real life: Your plan costs ₹0
- In JavaScript:
0: keep it - In real life: Your plan says no reminder
- In JavaScript:
false: keep it - In real life: Your plan has a blank name, on purpose
- In JavaScript:
"": keep it
The second tool is optional chaining, written ?.. It lets you read data that might be incomplete. You don't have to write a long check before every read.
The ?? backup operator
INTERACTIVEleft ?? right asks one question: "Is left equal to null or undefined?"
- If yes, the result is
right. - If no, the result is
left, unchanged.
const displayName = undefined;
const savedVolume = 0;
const city = null;
console.log(displayName ?? "Anonymous", savedVolume ?? 50, city ?? "Unknown");Line 1 sets displayName to undefined, so line 4 uses "Anonymous". Line 2 sets savedVolume to 0. That is a real value, so it stays 0. Line 3 sets city to null, so it becomes "Unknown". The output is Anonymous 0 Unknown.
0. || sees a falsy value and uses the backup, 10. ?? sees a value that is not null or undefined, so it keeps the 0.The two operators ask different questions. || asks, "Is the value falsy?" ?? asks only, "Is the value null or undefined?" Those are the two "nothing here" values from the Booleans, null & undefined lesson.
Like ||, ?? works from left to right. JavaScript looks at the left side first. It runs the right side only when the left side is null or undefined. This helps when the backup takes work, like calling a function.
const savedName = "Asha";
function makeGuest() {
console.log("backup checked");
return "Guest";
}
console.log(savedName ?? makeGuest());Line 1 stores the name "Asha". Lines 2 to 5 make a backup function. If it ran, it would print backup checked. Line 6 prints Asha. The backup function never runs, because "Asha" is not null or undefined.
const value = 0;console.log(value || "default");console.log(value ?? "default");- value
- 0
value || "default"- default
value ?? "default"- 0
The two operators give different results here. || replaces any falsy value. ?? replaces only null or undefined.
The table below tries each value with both operators. Read each row as: "when the value is this, || gives this, and ?? gives this."
| The value | What does || give? | What does ?? give? | Are they different? |
|---|---|---|---|
0 | "default" | 0 | Yes. 0 is falsy, but it is not nullish. |
"" | "default" | "" | Yes. An empty string can be a real value, chosen on purpose. |
false | "default" | false | Yes. false is a real yes-or-no answer. |
NaN | "default" | NaN | Yes. NaN is falsy, but it is not null or undefined. |
null | "default" | "default" | No. Both use the backup. |
undefined | "default" | "default" | No. Both use the backup. |
So || is not wrong. It asks a different question: "Is the left side falsy?" Use || when you want to replace every falsy value, such as "", 0, false, and NaN. Use ?? when those values can be real data.
Reading safely with ?.
STEP THROUGHIn the Logical operators lesson, you saw long checks like user && user.address && user.address.city. Each && checks that one part is there before the next part is read. Optional chaining does the same job in a shorter way: user?.address?.city.
Here is what ?. does:
- It looks at the value just before it.
- If that value is
nullorundefined, it stops. The result isundefined, and there is no error. - If not, it reads the next part as usual.
user holds null, so there is no address to read. A plain dot throws an error. ?. stops and gives undefined instead.?. checks that the door is thereYou walk down a hallway. Before you open the next door, you check that the door is there. If there is no door, you stop. You don't walk into the wall.
- In real life: There is no next door
- In JavaScript: The value before
?.isnullorundefined - In real life: You stop and turn back
- In JavaScript: The result is
undefined - In real life: The door is there
- In JavaScript: JavaScript opens it and keeps reading
- In real life: A door painted on the wall
- In JavaScript: The property is there but is not a function, so
?.()still throws
You can put ?. in three places:
- Before a property name:
obj?.name. - Before square brackets:
arr?.[0]orobj?.[key]. - Before call brackets:
fn?.()orobj.method?.(). The call happens only if the function is there.
const user = {
name: "Asha",
tags: ["reader"],
greet() {
return "Hi " + this.name;
},
};
console.log(user?.name, user?.tags?.[0], user.greet?.());Lines 1 to 7 make a user object. It has a name, a list of tags with one tag in it, and a greet method. Line 8 does three things: it reads the name, reads the first tag, and calls greet. Every part is there, so the output is Asha reader Hi Asha.
greet is called on user, so inside it, this is user. That is how greet can read the name.
Pick a user, then step through each read. Before each step, guess: will it give a value, undefined, or a caught TypeError?
script
let checks = 0;function mark(label) { checks += 1; return label;}function riskyCity() { try { return user.address.city; } catch (error) { return error.name; }}const safeCity = user?.address?.[mark("city")];const firstTag = user?.tags?.[0];const greeting = user?.greet?.();const noFunction = undefined;const optionalCall = noFunction?.();const notCallable = (() => { try { return ({ run: 7 }).run?.(); } catch (error) { return error.name; }})();const missingUser = null;const skipped = missingUser?.[mark("skipped")];console.log(riskyCity(), safeCity, firstTag, greeting, optionalCall, notCallable, checks, skipped);Remember: ?. checks only the value just before it. It stops only when that value is null or undefined. If the value is there but is the wrong kind of value, the normal rules apply. For example, trying to call the number 7 with ?.() still throws a TypeError.
??=, ||=, and &&=
STEP THROUGHThese three operators are called logical assignment operators. Each one checks a variable or property first. Then it stores a new value only if the check passes.
x ??= ystoresyonly ifxisnullorundefined. It fills in a missing value.x ||= ystoresyonly ifxis falsy. It replaces any falsy value.x &&= ystoresyonly ifxis truthy. It updates a value that is already there.
const settings = { volume: 0, theme: undefined, nickname: "Asha" };
settings.volume ??= 50;
settings.theme ??= "light";
settings.nickname &&= settings.nickname.trim();
console.log(settings.volume, settings.theme, settings.nickname);Line 1 makes a settings object. Line 2 keeps the volume at 0, because 0 is not nullish. Line 3 fills in the missing theme with "light". Line 4 runs its update because "Asha" is truthy. It stores the trimmed nickname. The .trim() method removes spaces from the start and end of a string. The output is 0 light Asha.
??= stores the backup only in an empty place. The volume holds 0, which is a real value, so it stays. The theme holds nothing, so it gets "light".Step through the three logical assignments. Watch ??= keep 0, ||= replace an empty string, and &&= update only a value that is truthy.
script
const user = { nickname: " Ada " };settings.volume ??= 50;settings.theme ||= "light";settings.retries &&= settings.retries + 1;user.nickname &&= user.nickname.trim();console.log(settings.volume, settings.theme, settings.retries, user.nickname);A common beginner mistake is using ||= for a setting where 0 is a real choice. volume ||= 50 would turn a muted volume back up to 50. Use ??=, so a muted volume stays muted.
Code that JavaScript refuses to run
SYNTAXJavaScript refuses to run a few lines that look fine at first. The first rule: you can't write ?? next to && or || without round brackets. The round brackets show which part runs first.
const a = null;
const b = "backup";
const c = "last";
console.log(a || b ?? c);const a = null;
const b = "backup";
const c = "last";
console.log((a || b) ?? c);In the valid version, the brackets tell JavaScript to work out a || b first. a is null, so a || b gives "backup". That value is not nullish, so ?? c keeps it. The output is backup.
The second rule: you can't put ?. on the left side of =. If obj were missing, JavaScript would have nowhere to store the new value. So it refuses the line before running anything.
const obj = { a: 1 };
obj?.a = 2;The last case is a different error. ?. can't help with a variable name that doesn't exist.
console.log(missingUser?.name);This throws a ReferenceError. The problem is not name. The problem is that JavaScript can't find a variable called missingUser at all. ?. can stop at a null or undefined value. It can't create a variable, and it can't fix a typo in a name.
Don't add ?. everywhere just to avoid errors. If a value must always be there, an error is useful. A TypeError tells you right away that something in your data or your program is wrong.
Where you’ll use this
You'll see these operators in code that reads user profiles, saved settings, optional functions, and data that comes from outside your program. Objects and arrays get their own lessons in Stage 3. For now, read them as simple containers:
{ name: "Ada" }is an object. It has named properties, likename.["admin"]is an array. It has items that you read by position, starting at0.
const user = { name: "Asha", address: { city: "Pune" }, nickname: "" };
const displayName = user.nickname ?? user.name ?? "Guest";
const city = user.address?.city ?? "Unknown";
const firstRole = user.roles?.[0] ?? "reader";
console.log(displayName, city, firstRole);Line 2 keeps the empty nickname, because ?? treats "" as a real value. Line 3 reads the city, "Pune". Line 4 finds no roles, so ?. gives undefined, and ?? uses "reader". Line 5 prints the empty nickname, then Pune reader.
What if your app says a blank nickname should use the name instead? Then line 2 should use ||. Pick the operator that matches the rule your app needs.
This matters when a form is only partly filled in. These are two different cases:
- The whole
contactobject is missing. - The
contactobject is there, but its phone is blank.
?. handles the first case. Then your app's rule decides if a blank phone is OK.
const profile = { contact: { phone: "" } };
const savedPhone = profile.contact?.phone ?? "No phone saved";
const displayPhone = profile.contact?.phone || "Add a phone number";
console.log(savedPhone, displayPhone);Line 1 makes a profile with a contact object. Its phone is an empty string, on purpose. Line 2 keeps that empty string, because ?? sees a real value. Line 3 gives "Add a phone number", because || replaces an empty string. Line 4 prints an empty value first, then Add a phone number.
Read profile.contact?.phone ?? "No phone saved" in two steps:
?.tries to read the phone. It givesundefinedonly if a part is missing.??replaces thatundefinedwith the backup text.
?. tries to read the phone and finds no contact, so it gives undefined. Then ?? sees undefined and uses the backup text.Each operator has its own job. That makes the code easy to change when your app's rule changes.
- A saved volume can be
0, which means muted. - A product price can be
0for a free item. - A username must not be empty, so
""should show Guest. - A menu label should use a backup when it is
"". - A saved checkbox can be
false, which means the person said no. - A search box should show
All itemswhen the search text is blank.
Sort each case. Pick ?? if falsy values like 0 or false should be kept. Pick || if they should be replaced.
Common misconceptions
- “Nullish means falsy.” No. Nullish means only
nullorundefined. Falsy also includes0,false,"", andNaN. - “
??is a better||.” No. They ask different questions, so each one is right for a different job. - “
?.makes a whole line safe.” No. It checks only the value just before it. - “
method?.()checks that it is a function.” No. It skips only a missing method. A value that is not a function still throws aTypeError. - “Logical assignment is just shorter.” It is shorter, and it also shows the exact check that decides whether to store.
- “Use
?.everywhere.” No. If data must be there, let the error show, so you can find the bug.
The table below compares six tools that look alike. Read each row as: "this tool checks this, it is useful for this, and watch out for this."
| Tool | What does it check? | When is it useful? | What should you watch for? |
|---|---|---|---|
?? | Is the value null or undefined? | It keeps 0, false, and blank strings. | It does not reject a blank string. |
|| | Is the value falsy? | It replaces any blank, zero, false, or NaN value. | It can replace a real choice, like a muted volume. |
?. | Is the value before it null or undefined? | It reads data that might not be there. | It can hide a bug when the data must be there. |
??= | Is the stored value null or undefined? | It fills in missing settings. | It keeps 0, on purpose. |
||= | Is the stored value falsy? | It replaces empty labels on a page. | It also replaces 0 and false. |
&&= | Is the stored value truthy? | It updates a string or flag that is already there. | It skips empty strings and 0. |
Practice exercises
5 EXERCISESWork out the output in your head. Type both output lines in order.
const saved = 0;
console.log(saved || 10);
console.log(saved ?? 10);const saved = 0;
console.log(saved || 10);
console.log(saved ?? 10);The first log uses ||. 0 is falsy, so it uses the backup, 10. The second log uses ??. 0 is not nullish, so it stays 0.
The app should keep a saved volume of 0. Change the operator that picks the default.
const savedVolume = 0;
const volume = savedVolume || 50;
console.log(volume);const savedVolume = 0;
const volume = savedVolume ?? 50;
console.log(volume);Use ??, so only null or undefined uses the backup, 50. The fixed program prints 0.
?.Replace the repeated && checks with optional chaining.
const city = user && user.address && user.address.city;
console.log(city || "Unknown");const user = { address: { city: "Pune" } };
console.log(user?.address?.city ?? "Unknown");user?.address?.city ?? "Unknown" does the same job as the long && check. It is shorter, and it keeps any value that is not nullish.
??=Write two logical assignment lines. Keep the saved volume, and add a theme.
const settings = { volume: 0 };
// Fill volume with 50 only if missing.
// Fill theme with "light" only if missing.
console.log(settings.volume, settings.theme);const settings = { volume: 0 };
settings.volume ??= 50;
settings.theme ??= "light";
console.log(settings.volume, settings.theme);settings.volume ??= 50 keeps 0. settings.theme ??= "light" fills in the missing theme.
?? sits next to ||What must you add to make line 4 valid JavaScript?
const a = null;
const b = "backup";
const c = "last";
console.log(a || b ?? c);const a = null;
const b = "backup";
const c = "last";
console.log((a || b) ?? c);JavaScript needs round brackets when ?? is next to || or &&. (a || b) ?? c is valid, and it prints backup.
A settings page saved two choices on purpose: no email updates, and a page size of 0. Predict the output after the two default lines run. Real websites use this same rule when they load saved settings.
const preferences = { emailUpdates: false, pageSize: 0 };
preferences.emailUpdates ??= true;
preferences.pageSize ??= 20;
console.log(preferences.emailUpdates, preferences.pageSize);const preferences = { emailUpdates: false, pageSize: 0 };
preferences.emailUpdates ??= true;
preferences.pageSize ??= 20;
console.log(preferences.emailUpdates, preferences.pageSize);Neither line changes the object. A person can choose not to get email updates. A page size of 0 can be a real saved choice. Neither value is nullish, so the program prints false 0.
Check your understanding
7 QUESTIONSQuestion 1 of 7When does
left ?? rightuseright?Choose an answer to see the explanation.
Question 2 of 7What does the
count ?? 10code print?Read the code, then predictconst count = 0; console.log(count ?? 10);Choose an answer to see the explanation.
Question 3 of 7What does this read of the user's city print?
Read the code, then predictconst user = { address: { city: "Pune" } }; console.log(user?.address?.city);Choose an answer to see the explanation.
Question 4 of 7What does this code print when the user is null?
Read the code, then predictconst user = null; console.log(user?.name ?? "Guest");Choose an answer to see the explanation.
Question 5 of 7Which line can throw an error even though it uses
?.?Choose an answer to see the explanation.
Question 6 of 7What does the
??=volume code print?Read the code, then predictconst settings = { volume: 0 }; settings.volume ??= 50; console.log(settings.volume);Choose an answer to see the explanation.
Question 7 of 7Which line is valid JavaScript?
Choose an answer to see the explanation.
Key takeaways
??uses the backup only fornullandundefined.||uses the backup for every falsy value, so it can lose0,false, and blank strings.?.givesundefinedwhen the value before it isnullorundefined.?.()skips a missing function, but a value that is not a function still throws aTypeError.??=,||=, and&&=store a new value only when their check passes.- Use round brackets when you put
??next to&&or||.
Final definition: ?? gives a backup for a missing value. ?. stops a property read or a call at a missing part and gives undefined.
Coming next: The rest of the operators.