Real data has gaps. A user may have no address, an API may leave out a field, a setting may never have been saved. Two operators handle these gaps cleanly: ?. reads through missing parts without throwing, and ?? supplies a default only when a value is genuinely missing. Both are supported in all current browsers and Node.js.
The problem
Reading a property of undefined or null throws:
const user = { name: 'Noor' };
console.log(user.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')
The old fix was a chain of checks:
const city = user && user.address && user.address.city;
It works, but it is noisy, and it is easy to get wrong.
Optional chaining: ?.
?. reads a property only if the value on its left is not null or undefined. If it is, the whole expression stops and gives undefined:
console.log(user.address?.city); // → undefined
console.log(user?.name); // → Noor
This is called short-circuiting. Once ?. meets a missing value, nothing to its right runs. That means one ?. protects the rest of the chain:
const order = { customer: null };
console.log(order.customer?.address.postcode); // → undefined
address.postcode is never evaluated because customer is null.
Three forms
| Form | Example | Use it for |
|---|---|---|
obj?.prop | user.address?.city | A property that may be missing |
obj?.[expr] | scores?.[0], user.settings?.[key] | Bracket access, including array items |
fn?.() | options.onSave?.(data) | Calling a function that may not exist |
The function form is handy for optional callbacks:
function save(data, options = {}) {
console.log('Saving', data);
options.onSuccess?.(data);
}
save({ id: 1 });
// → Saving { id: 1 }
save({ id: 2 }, { onSuccess: (d) => console.log(`Saved ${d.id}`) });
// → Saving { id: 2 }
// → Saved 2
With DOM code, ?. helps when an element might not be on every page:
document.querySelector('#newsletter')?.addEventListener('submit', handleSignup);
handleSignup here stands for whatever handler function you have defined.
Do not overuse it
?. hides missing data. That is right when the data is allowed to be missing, and wrong when it is not. If every user must have an id, write user.id, so a bug shows up as an error rather than a silent undefined that breaks something later. Put ?. only where a gap is expected.
You also cannot assign with it: user.address?.city = 'Leeds' is a SyntaxError.
Nullish coalescing: ??
a ?? b gives a, unless a is null or undefined, in which case it gives b. "Nullish" means exactly those two values.
const settings = { volume: 0, nickname: '' };
console.log(settings.volume ?? 50); // → 0
console.log(settings.nickname ?? 'Guest'); // → (an empty string)
console.log(settings.theme ?? 'light'); // → light
Why not ||?
Before ??, people wrote defaults with ||. But || falls back on any falsy value, and 0, '', false and NaN are falsy:
console.log(settings.volume || 50); // → 50 (wrong: the user chose 0)
console.log(settings.nickname || 'Guest'); // → Guest
The volume bug is a classic: the user turns the sound off, and the app turns it back up.
Value of x | x || 'default' | x ?? 'default' |
|---|---|---|
undefined | 'default' | 'default' |
null | 'default' | 'default' |
0 | 'default' | 0 |
'' | 'default' | '' |
false | 'default' | false |
'Ana' | 'Ana' | 'Ana' |
Use ?? when 0, false or an empty string are valid values. Use || only when you really do want every falsy value replaced, for example treating an empty search box the same as no search.
Using them together
?. and ?? are designed to be combined: read safely, then fall back.
const response = {
data: {
course: { title: 'JavaScript', stats: { lessons: 0 } },
},
};
const lessons = response.data?.course?.stats?.lessons ?? 'not counted yet';
console.log(lessons); // → 0
const author = response.data?.course?.author?.name ?? 'Unknown author';
console.log(author); // → Unknown author
This pattern is common with JSON from a server, which you will start loading in lesson 33.
Mixing with && and ||
You cannot mix ?? with && or || without brackets. JavaScript refuses to guess what you meant:
const a = null;
// const result = a || b ?? 'c'; SyntaxError
const result = (a || 0) ?? 'c';
console.log(result); // → 0
Default assignment: ??=
x ??= value assigns only if x is currently null or undefined. It is shorthand for x = x ?? value, except that it does not assign at all when x already has a value:
const prefs = { fontSize: 0 };
prefs.fontSize ??= 16;
prefs.theme ??= 'light';
console.log(prefs); // → { fontSize: 0, theme: 'light' }
It is useful for filling in missing options and for lazy caches:
const cache = {};
function getGreeting(language) {
cache[language] ??= `Built greeting for ${language}`;
return cache[language];
}
console.log(getGreeting('en')); // → Built greeting for en
Its siblings ||= and &&= assign on falsy and truthy values respectively. The same rule applies: pick ??= unless you mean "any falsy value".
Defaults in parameters versus ??
Default parameter values and destructuring defaults (lesson 23) also apply only to undefined, but not to null:
function greet(name = 'friend') {
return `Hello, ${name}`;
}
console.log(greet(undefined)); // → Hello, friend
console.log(greet(null)); // → Hello, null
When null can arrive, for example from JSON where missing values are often null, use ?? inside the function instead:
function greetSafely(name) {
return `Hello, ${name ?? 'friend'}`;
}
console.log(greetSafely(null)); // → Hello, friend
Summary
?.reads a property, item or method only when the left side is notnullorundefined, and short-circuits the rest of the chain.- Use
?.only where missing data is expected, so real bugs still throw. ??falls back only onnullandundefined;||falls back on every falsy value, including0and''.- Brackets are required when mixing
??with&&or||. ??=fills in missing values, and default parameters do not catchnull, so use??whennullis possible.