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:
nullundefined// 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 cacheconst 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 💪