Nullish Coalescing in JavaScript
What is Nullish Coalescing?
The nullish coalescing operator (??) is an ES2020 feature that returns the right-hand operand when the left-hand operand is null or undefined, and otherwise returns the left-hand operand.
Syntax
leftExpression ?? rightExpressionBasic Usage
const value1 = null ?? 'default';
console.log(value1); // 'default'
const value2 = undefined ?? 'default';
console.log(value2); // 'default'
const value3 = 'actual value' ?? 'default';
console.log(value3); // 'actual value'
const value4 = 0 ?? 'default';
console.log(value4); // 0 (not 'default')
const value5 = '' ?? 'default';
console.log(value5); // '' (not 'default')
const value6 = false ?? 'default';
console.log(value6); // false (not 'default')Nullish vs Falsy
The Key Difference
// Falsy values in JavaScript:
// false, 0, -0, 0n, '', null, undefined, NaN
// OR operator (||) treats ALL falsy values as "no value"
console.log(0 || 'default'); // 'default'
console.log('' || 'default'); // 'default'
console.log(false || 'default'); // 'default'
console.log(null || 'default'); // 'default'
// Nullish coalescing (??) only treats null and undefined as "no value"
console.log(0 ?? 'default'); // 0
console.log('' ?? 'default'); // ''
console.log(false ?? 'default'); // false
console.log(null ?? 'default'); // 'default'When This Matters
// User settings example
const userSettings = {
notifications: false, // User explicitly disabled
volume: 0, // User set to mute
theme: '', // User cleared theme
language: null // Not set
};
// Using OR (||) - WRONG
const notifications = userSettings.notifications || true; // true (wrong!)
const volume = userSettings.volume || 50; // 50 (wrong!)
const theme = userSettings.theme || 'dark'; // 'dark' (wrong!)
const language = userSettings.language || 'en'; // 'en' (correct)
// Using nullish coalescing (??) - CORRECT
const notifications2 = userSettings.notifications ?? true; // false (correct!)
const volume2 = userSettings.volume ?? 50; // 0 (correct!)
const theme2 = userSettings.theme ?? 'dark'; // '' (correct!)
const language2 = userSettings.language ?? 'en'; // 'en' (correct)Practical Examples
1. Configuration with Defaults
function createConfig(options) {
return {
host: options?.host ?? 'localhost',
port: options?.port ?? 3000,
debug: options?.debug ?? false,
timeout: options?.timeout ?? 5000,
retries: options?.retries ?? 3,
cache: options?.cache ?? true
};
}
// All defaults
console.log(createConfig({}));
// { host: 'localhost', port: 3000, debug: false, ... }
// With explicit false/0 values
console.log(createConfig({ port: 0, debug: false, cache: false }));
// { host: 'localhost', port: 0, debug: false, cache: false, ... }
// With OR operator (wrong)
function createConfigWrong(options) {
return {
port: options?.port || 3000, // 0 becomes 3000 (wrong!)
debug: options?.debug || false // false stays false (but for wrong reason)
};
}2. API Response Handling
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return {
name: data.name ?? 'Anonymous',
age: data.age ?? 0, // 0 is valid age
email: data.email ?? 'no-email@example.com',
isActive: data.isActive ?? true,
score: data.score ?? 0, // 0 is valid score
bio: data.bio ?? 'No bio available'
};
}3. Form Input Handling
function processFormData(formData) {
// Handle empty strings vs null/undefined
const username = formData.username ?? '';
const email = formData.email ?? '';
const age = formData.age ?? null;
const newsletter = formData.newsletter ?? false;
// Validate
const errors = [];
if (username.trim() === '') {
errors.push('Username is required');
}
if (email.trim() === '') {
errors.push('Email is required');
}
return { username, email, age, newsletter, errors };
}
// Empty form
console.log(processFormData({}));
// { username: '', email: '', age: null, newsletter: false, errors: [...] }
// Partial form
console.log(processFormData({ username: 'john', age: 0 }));
// { username: 'john', email: '', age: 0, newsletter: false, errors: [...] }4. Database Query Results
function formatUser(dbResult) {
return {
id: dbResult.id,
name: dbResult.name ?? 'Unknown User',
email: dbResult.email ?? null,
loginCount: dbResult.login_count ?? 0, // 0 is valid
lastLogin: dbResult.last_login ?? null,
isVerified: dbResult.is_verified ?? false, // false is valid
role: dbResult.role ?? 'user'
};
}5. Local Storage with Defaults
function getStoredValue(key, defaultValue) {
const stored = localStorage.getItem(key);
// Parse JSON if it's a string
if (stored !== null) {
try {
return JSON.parse(stored);
} catch {
return stored;
}
}
return defaultValue;
}
// Usage
const theme = getStoredValue('theme', 'light');
const volume = getStoredValue('volume', 50);
const autoplay = getStoredValue('autoplay', true);
// Store values
function setStoredValue(key, value) {
const stringValue = typeof value === 'string'
? value
: JSON.stringify(value);
localStorage.setItem(key, stringValue);
}Chaining with Optional Chaining
const user = {
profile: {
settings: {
theme: null,
notifications: false
}
}
};
// Combine optional chaining with nullish coalescing
const theme = user?.profile?.settings?.theme ?? 'default';
const notifications = user?.profile?.settings?.notifications ?? true;
const language = user?.profile?.settings?.language ?? 'en';
console.log(theme); // 'default'
console.log(notifications); // false (not true!)
console.log(language); // 'en'Multiple Nullish Coalescing
// Chain multiple nullish coalescing operators
const value = option1 ?? option2 ?? option3 ?? 'default';
// Practical example: fallback chain
function getConfig(env) {
const config = {
apiUrl: env.API_URL ?? process.env.API_URL ?? 'http://localhost:3000',
apiKey: env.API_KEY ?? process.env.API_KEY ?? null,
timeout: env.TIMEOUT ?? process.env.TIMEOUT ?? 5000
};
return config;
}Assignment with Nullish Coalescing
Nullish Coalescing Assignment (??=)
let config = {
host: 'localhost',
port: null
};
// Only assign if current value is null or undefined
config.host ??= 'default-host'; // No change (already has value)
config.port ??= 3000; // Assigns 3000 (was null)
config.timeout ??= 5000; // Assigns 5000 (didn't exist)
console.log(config);
// { host: 'localhost', port: 3000, timeout: 5000 }
// Compare with OR assignment (||=)
let settings = {
volume: 0,
muted: false
};
settings.volume ||= 50; // Assigns 50 (0 is falsy)
settings.muted ||= true; // Assigns true (false is falsy)
// Better with ??=
settings.volume ??= 50; // No change (0 is not null/undefined)
settings.muted ??= true; // No change (false is not null/undefined)Common Patterns
1. Function Parameters with Defaults
function createUser(options = {}) {
return {
name: options.name ?? 'Anonymous',
age: options.age ?? null,
isActive: options.isActive ?? true,
role: options.role ?? 'user',
permissions: options.permissions ?? []
};
}
// Works with explicit false/0 values
const user1 = createUser({ age: 0, isActive: false });
console.log(user1);
// { name: 'Anonymous', age: 0, isActive: false, role: 'user', permissions: [] }2. Pagination Defaults
function getPaginationParams(query) {
return {
page: parseInt(query.page) || 1, // Use || for page (0 is invalid)
limit: parseInt(query.limit) ?? 10, // Use ?? for limit (0 might be valid)
offset: parseInt(query.offset) ?? 0, // Use ?? for offset (0 is valid)
sortBy: query.sortBy ?? 'createdAt',
order: query.order ?? 'desc'
};
}3. Feature Flags
const features = {
newUI: null, // Not decided
darkMode: false, // Explicitly disabled
analytics: true, // Enabled
betaFeatures: undefined // Not set
};
function isFeatureEnabled(featureName) {
// Use ?? to respect explicit false values
return features[featureName] ?? false;
}
console.log(isFeatureEnabled('newUI')); // false (default)
console.log(isFeatureEnabled('darkMode')); // false (explicit)
console.log(isFeatureEnabled('analytics')); // true
console.log(isFeatureEnabled('betaFeatures')); // false (default)4. Error Messages
function displayError(error) {
const message = error?.message ?? 'An unknown error occurred';
const code = error?.code ?? 'UNKNOWN_ERROR';
const details = error?.details ?? null;
console.error(`[${code}] ${message}`);
if (details) {
console.error('Details:', details);
}
}Edge Cases
1. With Numbers
const count = 0;
const defaultCount = 10;
// Wrong: treats 0 as "no value"
const result1 = count || defaultCount; // 10
// Correct: 0 is a valid value
const result2 = count ?? defaultCount; // 02. With Booleans
const isEnabled = false;
// Wrong: treats false as "no value"
const result1 = isEnabled || true; // true
// Correct: false is a valid value
const result2 = isEnabled ?? true; // false3. With Empty Strings
const userInput = '';
// Wrong: treats empty string as "no value"
const result1 = userInput || 'default'; // 'default'
// Correct: empty string is a valid value
const result2 = userInput ?? 'default'; // ''
// When you actually want to treat empty string as "no value"
const result3 = userInput || 'default'; // Use || intentionally
// Or be explicit
const result4 = (userInput !== null && userInput !== undefined && userInput !== '')
? userInput
: 'default';4. Cannot Mix with && or ||
// This is INVALID - causes SyntaxError
// const result = value1 ?? value2 || value3;
// Must use parentheses
const result = (value1 ?? value2) || value3;
// or
const result2 = value1 ?? (value2 || value3);Performance Considerations
// Nullish coalescing is very efficient
// It short-circuits - right side only evaluated if needed
let expensiveCallCount = 0;
function expensiveOperation() {
expensiveCallCount++;
return 'expensive result';
}
const value = 'exists';
const result = value ?? expensiveOperation();
console.log(result); // 'exists'
console.log(expensiveCallCount); // 0 (function not called)Browser Support
// Nullish coalescing is supported in:
// - Chrome 80+
// - Firefox 72+
// - Safari 13.1+
// - Edge 80+
// - Node.js 14+
// Babel transpilation example:
// Input:
const value = input ?? 'default';
// Output (simplified):
const value = input !== null && input !== void 0 ? input : 'default';Best Practices
- Use ?? for optional values: When
null/undefinedmeans “no value” - Use || for validation: When any falsy value should trigger default
- Be explicit with empty strings: Decide if
''is valid or not - Document your choice: Make it clear why you chose ?? vs ||
- Combine with optional chaining:
obj?.prop ?? default - Use ??= for lazy initialization: Only assign if null/undefined
- Consider TypeScript: Better type safety with nullish coalescing
Interview Tips
- Explain the difference between ?? and ||
- Show falsy vs nullish with concrete examples
- Demonstrate practical use cases: Settings, API responses, form data
- Discuss when to use each: ?? for optional, || for validation
- Mention short-circuit evaluation: Right side only evaluated if needed
- Show combination with ?.: Optional chaining + nullish coalescing
- Explain ??= operator: Nullish coalescing assignment
- Discuss edge cases: Empty strings, 0, false values
Summary
The nullish coalescing operator (??) provides a precise way to handle default values by only treating null and undefined as “no value”, unlike the OR operator (||) which treats all falsy values the same way. This is crucial when working with boolean flags, numeric values like 0, or empty strings that are intentionally set by users. Combined with optional chaining (?.), it creates a powerful pattern for safely accessing and providing defaults for potentially missing data.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.