The nullish coalescing operator ??
(Nullish Coalescing) returns the right operand when the left is equal to null
or undefined
, unlike the ||
operator, which checks for “falsiness”:
// Operator ??
const name = null ?? "Guest"; // "Guest"
const age = 0 ?? 18; // 0 (not replaced!)
const count = "" ?? "empty"; // "" (not replaced!)
// Comparison with ||
const name2 = null || "Guest"; // "Guest"
const age2 = 0 || 18; // 18 (replaced!)
const count2 = "" || "empty"; // "empty" (replaced!)
Recommendation: Use ??
for working with null
/undefined
, and ||
for working with “falsy” values.
// Basic syntax
const result = leftValue ?? rightValue;
// Examples
const userName = user.name ?? "Anonymous user";
const userAge = user.age ?? 0;
const userSettings = user.settings ?? {};
The ??
operator returns the right operand only if the left equals:
null
undefined
// Triggers (returns right operand)
console.log(null ?? "default"); // "default"
console.log(undefined ?? "default"); // "default"
// Does NOT trigger (returns left operand)
console.log(0 ?? "default"); // 0
console.log("" ?? "default"); // ""
console.log(false ?? "default"); // false
console.log(NaN ?? "default"); // NaN
Value | ?? “default” | || “default” | Explanation |
---|---|---|---|
null | "default" | "default" | Both operators replace |
undefined | "default" | "default" | Both operators replace |
0 | 0 | "default" | ?? preserves, || replaces |
"" | "" | "default" | ?? preserves, || replaces |
false | false | "default" | ?? preserves, || replaces |
NaN | NaN | "default" | ?? preserves, || replaces |
"text" | "text" | "text" | Both preserve |
42 | 42 | 42 | Both preserve |
// Working with numbers
const score = 0;
console.log(score ?? 100); // 0 (preserves zero)
console.log(score || 100); // 100 (replaces zero)
// Working with strings
const message = "";
console.log(message ?? "No message"); // "" (preserves empty string)
console.log(message || "No message"); // "No message" (replaces)
// Working with boolean
const isVisible = false;
console.log(isVisible ?? true); // false (preserves false)
console.log(isVisible || true); // true (replaces false)
// ❌ Problem with ||
function getUserSettings(user) {
return {
theme: user.theme || "light", // Problem: "" becomes "light"
fontSize: user.fontSize || 16, // Problem: 0 becomes 16
notifications: user.notifications || true // Problem: false becomes true
};
}
const user = {
theme: "", // User wants empty theme
fontSize: 0, // User wants minimum size
notifications: false // User disabled notifications
};
console.log(getUserSettings(user));
// { theme: "light", fontSize: 16, notifications: true } - NOT what user wanted!
// ✅ Correct with ??
function getUserSettingsCorrect(user) {
return {
theme: user.theme ?? "light",
fontSize: user.fontSize ?? 16,
notifications: user.notifications ?? true
};
}
console.log(getUserSettingsCorrect(user));
// { theme: "", fontSize: 0, notifications: false } - Exactly what user wanted!
// Processing API response
function processApiResponse(response) {
// ❌ Bad with ||
const badResult = {
id: response.id || "unknown",
count: response.count || 0, // Problem: if count = 0
isActive: response.isActive || false // Problem: if isActive = false
};
// ✅ Good with ??
const goodResult = {
id: response.id ?? "unknown",
count: response.count ?? 0,
isActive: response.isActive ?? false
};
return goodResult;
}
// Test data
const apiResponse = {
id: "user123",
count: 0, // Valid value!
isActive: false // Valid value!
};
console.log(processApiResponse(apiResponse));
// { id: "user123", count: 0, isActive: false }
// Loading configuration
function loadConfig(userConfig = {}) {
const defaultConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
debug: false
};
return {
apiUrl: userConfig.apiUrl ?? defaultConfig.apiUrl,
timeout: userConfig.timeout ?? defaultConfig.timeout,
retries: userConfig.retries ?? defaultConfig.retries,
debug: userConfig.debug ?? defaultConfig.debug
};
}
// User wants to disable debugging and set 0 retries
const userConfig = {
timeout: 0, // No timeout
retries: 0, // No retries
debug: false // No debugging
};
console.log(loadConfig(userConfig));
// Will preserve user settings: timeout: 0, retries: 0, debug: false
// Checking multiple sources
const userName =
user.preferredName ??
user.firstName ??
user.email ??
"Guest";
// Equivalent to:
let userName2;
if (user.preferredName !== null && user.preferredName !== undefined) {
userName2 = user.preferredName;
} else if (user.firstName !== null && user.firstName !== undefined) {
userName2 = user.firstName;
} else if (user.email !== null && user.email !== undefined) {
userName2 = user.email;
} else {
userName2 = "Guest";
}
// Safe access to nested properties
const city = user?.address?.city ?? "Not specified";
const phone = user?.contacts?.phone ?? "Not specified";
const avatar = user?.profile?.avatar?.url ?? "/default-avatar.png";
// Working with arrays
const firstItem = data?.items?.[0] ?? "No items";
const lastItem = data?.items?.at?.(-1) ?? "No items";
// Operator ??= (ES2021)
let config = {};
// Sets value only if it's null or undefined
config.theme ??= "dark";
config.language ??= "en";
config.timeout ??= 5000;
console.log(config);
// { theme: "dark", language: "en", timeout: 5000 }
// Doesn't overwrite existing values
config.theme = "light";
config.theme ??= "dark"; // Won't change
console.log(config.theme); // "light"
// Cache initialization
class DataCache {
constructor() {
this.cache = {};
}
get(key) {
// Create cache entry only if it doesn't exist
this.cache[key] ??= this.fetchData(key);
return this.cache[key];
}
fetchData(key) {
console.log(`Loading data for ${key}`);
return `Data for ${key}`;
}
}
const cache = new DataCache();
console.log(cache.get("user1")); // Loading data for user1
console.log(cache.get("user1")); // From cache
const a = 0;
const b = "";
const c = false;
const d = null;
console.log(a ?? "default");
console.log(b ?? "default");
console.log(c ?? "default");
console.log(d ?? "default");
0
, ""
, false
, "default"
a ?? "default"
→ 0
(zero is not equal to null/undefined)b ?? "default"
→ ""
(empty string is not equal to null/undefined)c ?? "default"
→ false
(false is not equal to null/undefined)d ?? "default"
→ "default"
(null is replaced)// ❌ Problematic function
function createUser(data) {
return {
name: data.name || "User",
age: data.age || 18,
isAdmin: data.isAdmin || false,
score: data.score || 0
};
}
// Test data
const userData = {
name: "", // User didn't specify name
age: 0, // Newborn
isAdmin: false, // Regular user
score: 0 // Initial score
};
console.log(createUser(userData));
// What will be wrong?
Problem: All values will be replaced with default values due to the ||
operator.
Result: { name: "User", age: 18, isAdmin: false, score: 0 }
Fixed version:
function createUser(data) {
return {
name: data.name ?? "User",
age: data.age ?? 18,
isAdmin: data.isAdmin ?? false,
score: data.score ?? 0
};
}
// Now result: { name: "", age: 0, isAdmin: false, score: 0 }
const user = {
profile: {
social: {
twitter: null,
facebook: undefined,
instagram: ""
}
}
};
// Find first available social network
const socialNetwork =
user.profile.social.twitter ??
user.profile.social.facebook ??
user.profile.social.instagram ??
"Not specified";
console.log(socialNetwork); // ?
""
(empty string)
twitter
equals null
→ proceed to nextfacebook
equals undefined
→ proceed to nextinstagram
equals ""
→ return ""
(empty string is not equal to null/undefined)const settings = {
theme: "dark",
language: null,
notifications: undefined
};
settings.theme ??= "light";
settings.language ??= "en";
settings.notifications ??= true;
settings.newFeature ??= "enabled";
console.log(settings);
{
theme: "dark", // Didn't change (already had value)
language: "en", // Changed (was null)
notifications: true, // Changed (was undefined)
newFeature: "enabled" // Added (didn't exist)
}
const response = {
data: {
users: [
{ id: 1, profile: { name: "Anna" } },
{ id: 2, profile: null },
{ id: 3 }
]
}
};
// Get names of all users with fallback
const names = response.data.users.map(user =>
user.profile?.name ?? "Name not specified"
);
console.log(names);
["Anna", "Name not specified", "Name not specified"]
profile.name
exists → "Anna"
profile
equals null
→ ?.name
returns undefined
→ ??
returns "Name not specified"
profile
doesn’t exist → ?.name
returns undefined
→ ??
returns "Name not specified"
// ✅ Good: for checking null/undefined
const userName = user.name ?? "Guest";
const userAge = user.age ?? 0;
// ❌ Bad: for checking "falsy" values
const isVisible = element.visible ?? true; // Use || for boolean
// ✅ Good: safe property access
const city = user?.address?.city ?? "Not specified";
const avatar = user?.profile?.avatar?.url ?? "/default.png";
// ❌ Bad: long checks
const city2 = (user && user.address && user.address.city) ?? "Not specified";
// ✅ Good: property initialization
config.timeout ??= 5000;
config.retries ??= 3;
// ❌ Bad: redundant checks
if (config.timeout === null || config.timeout === undefined) {
config.timeout = 5000;
}
// ✅ Good: clear that we only check null/undefined
function processData(data) {
// Use empty object only if data is not passed
const safeData = data ?? {};
// Use 0 as valid value for counter
const count = safeData.count ?? 0;
return { count };
}
// ❌ Bad: unclear what we're checking
function processDataBad(data) {
const safeData = data || {}; // Will replace empty objects too!
const count = safeData.count || 0; // Will replace valid zero!
return { count };
}
// Simple polyfill for ??
if (!window.nullishCoalescing) {
function nullishCoalescing(left, right) {
return (left !== null && left !== undefined) ? left : right;
}
// Usage
const result = nullishCoalescing(value, "default");
}
// Or use Babel for automatic transformation
// Three new assignment operators
let a, b, c;
// Nullish coalescing assignment
a ??= "default"; // a = a ?? "default"
// Logical OR assignment
b ||= "default"; // b = b || "default"
// Logical AND assignment
c &&= "value"; // c = c && "value"
// Powerful combinations
const userPreferences = {
theme: user?.settings?.theme ??
localStorage.getItem("theme") ??
"system",
language: user?.profile?.language ??
navigator.language?.split("-")?.[0] ??
"en"
};
// Conditional assignment
user.lastLogin ??= new Date();
user.preferences ??= {};
user.preferences.notifications ??= true;
// ❌ Bad: wrong operator choice
function setVolume(volume) {
// Problem: volume = 0 will be replaced with 50
this.volume = volume || 50;
}
// ✅ Good: correct choice
function setVolume(volume) {
// 0 is a valid value for volume
this.volume = volume ?? 50;
}
// ❌ Bad: ?? doesn't suit boolean logic
function isFeatureEnabled(flag) {
// Problem: false won't be replaced with true
return flag ?? true;
}
// ✅ Good: use || for boolean
function isFeatureEnabled(flag) {
return flag !== false; // Or other logic
}
// ❌ Bad: redundant
const value = (data !== null && data !== undefined) ? data : "default";
// ✅ Good: concise
const value = data ?? "default";
Golden Rules:
??
for checking null
/undefined
— it’s precise and predictable||
for checking “falsy” values — when you need to replace 0
, ""
, false
?.
) — for safe property access??=
for initialization — concise and readableRemember: The ??
operator solves a specific task — working with null
and undefined
. Don’t try to use it everywhere, choose the right tool for each situation.
Want more articles for interview preparation? Subscribe to EasyAdvice, bookmark the site and level up every day 💪