Back to Blog
JavaScriptNullish CoalescingES2020Operators

JavaScript Nullish Coalescing Guide

Master the nullish coalescing operator for handling null and undefined values.

B
Bootspring Team
Engineering
August 24, 2018
8 min read

The nullish coalescing operator (??) provides a better way to handle default values when dealing with null or undefined.

Basic Syntax#

1// ?? returns right-hand side when left is null or undefined 2const value1 = null ?? 'default'; // 'default' 3const value2 = undefined ?? 'default'; // 'default' 4const value3 = 'hello' ?? 'default'; // 'hello' 5 6// Compare with || (OR) 7const a = '' || 'default'; // 'default' (empty string is falsy) 8const b = '' ?? 'default'; // '' (empty string is NOT nullish) 9 10const c = 0 || 100; // 100 (0 is falsy) 11const d = 0 ?? 100; // 0 (0 is NOT nullish) 12 13const e = false || true; // true (false is falsy) 14const f = false ?? true; // false (false is NOT nullish)

Why Use Nullish Coalescing#

1// Problem with || operator 2const config = { 3 port: 0, // Valid port 4 timeout: '', // Intentionally empty 5 debug: false, // Intentionally false 6 retries: null // Not set 7}; 8 9// || treats falsy values as "not set" 10config.port || 3000; // 3000 (wrong! 0 is valid) 11config.timeout || '5s'; // '5s' (wrong! '' was intentional) 12config.debug || true; // true (wrong! false was intentional) 13config.retries || 3; // 3 (correct) 14 15// ?? only falls back for null/undefined 16config.port ?? 3000; // 0 (correct!) 17config.timeout ?? '5s'; // '' (correct!) 18config.debug ?? true; // false (correct!) 19config.retries ?? 3; // 3 (correct!)

With Optional Chaining#

1const user = { 2 name: 'John', 3 settings: { 4 theme: 'dark' 5 } 6}; 7 8// Combine ?. and ?? 9const theme = user?.settings?.theme ?? 'light'; 10// 'dark' 11 12const language = user?.settings?.language ?? 'en'; 13// 'en' 14 15// Deep nested access with defaults 16const config = {}; 17const port = config?.server?.port ?? 3000; 18// 3000 19 20const host = config?.server?.host ?? 'localhost'; 21// 'localhost' 22 23// Array access 24const users = []; 25const firstName = users?.[0]?.name ?? 'Guest'; 26// 'Guest'

Function Parameters#

1// Default parameter values 2function greet(name) { 3 const displayName = name ?? 'Guest'; 4 return `Hello, ${displayName}!`; 5} 6 7greet('John'); // 'Hello, John!' 8greet(undefined); // 'Hello, Guest!' 9greet(null); // 'Hello, Guest!' 10greet(''); // 'Hello, !' (empty string is not nullish) 11 12// Compare with default parameters 13function greetDefault(name = 'Guest') { 14 return `Hello, ${name}!`; 15} 16 17greetDefault('John'); // 'Hello, John!' 18greetDefault(undefined); // 'Hello, Guest!' (same) 19greetDefault(null); // 'Hello, null!' (different!) 20 21// Use both for complete coverage 22function process(value = getDefault()) { 23 // Default param handles undefined 24 const result = value ?? fallbackValue; 25 // ?? handles null passed explicitly 26 return result; 27}

Object Defaults#

1// Setting object property defaults 2function createUser(options) { 3 return { 4 name: options.name ?? 'Anonymous', 5 age: options.age ?? 0, 6 active: options.active ?? true, 7 bio: options.bio ?? '' 8 }; 9} 10 11createUser({ name: 'John' }); 12// { name: 'John', age: 0, active: true, bio: '' } 13 14createUser({ name: 'Jane', age: null, active: false }); 15// { name: 'Jane', age: 0, active: false, bio: '' } 16 17// Merging with defaults 18const defaults = { 19 theme: 'light', 20 fontSize: 14, 21 showSidebar: true 22}; 23 24function applyConfig(userConfig) { 25 return { 26 theme: userConfig.theme ?? defaults.theme, 27 fontSize: userConfig.fontSize ?? defaults.fontSize, 28 showSidebar: userConfig.showSidebar ?? defaults.showSidebar 29 }; 30} 31 32applyConfig({ theme: 'dark', fontSize: 0 }); 33// { theme: 'dark', fontSize: 0, showSidebar: true }

Nullish Coalescing Assignment#

1// ??= assigns if left side is null or undefined 2let a = null; 3a ??= 'default'; 4console.log(a); // 'default' 5 6let b = 'existing'; 7b ??= 'default'; 8console.log(b); // 'existing' 9 10let c = ''; 11c ??= 'default'; 12console.log(c); // '' (empty string kept) 13 14let d = 0; 15d ??= 100; 16console.log(d); // 0 (zero kept) 17 18// Practical use: lazy initialization 19const cache = {}; 20 21function getOrCompute(key, computeFn) { 22 return cache[key] ??= computeFn(); 23} 24 25getOrCompute('data', () => expensiveOperation()); 26// First call: computes and caches 27// Second call: returns cached value

Chaining#

1// Chain multiple fallbacks 2const a = null; 3const b = undefined; 4const c = ''; 5const d = 'value'; 6 7const result = a ?? b ?? c ?? d; 8// '' (c is not nullish, so it's returned) 9 10// Different from || 11const result2 = a || b || c || d; 12// 'value' (c is falsy, so continues to d) 13 14// Practical example 15function getValue() { 16 return localStorage.getItem('value') 17 ?? sessionStorage.getItem('value') 18 ?? cookies.get('value') 19 ?? defaultValue; 20}

With Logical Operators#

1// Cannot directly mix with && or || without parentheses 2// const result = a || b ?? c; // SyntaxError 3 4// Use parentheses to clarify 5const result1 = (a || b) ?? c; // OK 6const result2 = a || (b ?? c); // OK 7 8// Example 9const value = null; 10const fallback = 'fallback'; 11const override = false; 12 13// Get value, then apply override 14const final = (value ?? fallback) && !override; 15// 'fallback' && true = 'fallback' (truthy but not useful) 16 17// Better approach 18const result = override ? null : (value ?? fallback); 19// 'fallback'

Type Coercion#

1// ?? doesn't coerce types 2const a = null ?? 0; 3console.log(a, typeof a); // 0, 'number' 4 5const b = undefined ?? ''; 6console.log(b, typeof b); // '', 'string' 7 8const c = null ?? false; 9console.log(c, typeof c); // false, 'boolean' 10 11// vs || which can change types unexpectedly 12const d = 0 || 'default'; 13console.log(d, typeof d); // 'default', 'string' (type changed!) 14 15const e = 0 ?? 'default'; 16console.log(e, typeof e); // 0, 'number' (type preserved)

Common Use Cases#

1// API response handling 2async function fetchUser(id) { 3 const response = await fetch(`/api/users/${id}`); 4 const data = await response.json(); 5 6 return { 7 name: data.name ?? 'Unknown', 8 email: data.email ?? 'N/A', 9 avatar: data.avatar ?? '/default-avatar.png', 10 bio: data.bio ?? '', 11 followers: data.followers ?? 0 12 }; 13} 14 15// Configuration 16const userConfig = loadUserConfig(); 17const config = { 18 port: userConfig.port ?? process.env.PORT ?? 3000, 19 host: userConfig.host ?? process.env.HOST ?? 'localhost', 20 debug: userConfig.debug ?? process.env.DEBUG === 'true' ?? false 21}; 22 23// DOM element properties 24function getElementText(selector) { 25 const element = document.querySelector(selector); 26 return element?.textContent ?? 'Element not found'; 27} 28 29// State management 30function reducer(state, action) { 31 switch (action.type) { 32 case 'SET_VALUE': 33 return { 34 ...state, 35 value: action.payload ?? state.value 36 }; 37 default: 38 return state; 39 } 40}

TypeScript Integration#

1// TypeScript understands nullish coalescing 2function process(value: string | null | undefined): string { 3 return value ?? 'default'; 4 // Return type is string (not string | null | undefined) 5} 6 7// With optional properties 8interface Config { 9 port?: number; 10 host?: string; 11} 12 13function createConfig(config: Config) { 14 return { 15 port: config.port ?? 3000, 16 host: config.host ?? 'localhost' 17 }; 18} 19// Return type: { port: number; host: string } 20 21// Strict null checks 22function getValue(map: Map<string, string>, key: string): string { 23 const value = map.get(key); 24 return value ?? 'not found'; 25 // TypeScript knows value could be undefined 26}

Comparison Summary#

1// Summary of || vs ?? 2 3// Values treated as "empty" by ||: 4// false, 0, -0, 0n, '', null, undefined, NaN 5 6// Values treated as "empty" by ??: 7// null, undefined (only these two!) 8 9// Use || when: 10// - Any falsy value should trigger default 11// - You want "truthy or default" logic 12 13// Use ?? when: 14// - Only null/undefined should trigger default 15// - 0, '', false are valid values 16// - You want "nullish or default" logic 17 18const examples = [ 19 { value: null, '||': 'default', '??': 'default' }, 20 { value: undefined, '||': 'default', '??': 'default' }, 21 { value: false, '||': 'default', '??': false }, 22 { value: 0, '||': 'default', '??': 0 }, 23 { value: '', '||': 'default', '??': '' }, 24 { value: NaN, '||': 'default', '??': NaN }, 25];

Best Practices#

When to Use ??: ✓ Default values for optional config ✓ API response fallbacks ✓ User input defaults ✓ When 0, '', or false are valid When to Use ||: ✓ When any falsy value means "use default" ✓ Boolean coercion is desired ✓ Legacy code compatibility With Optional Chaining: ✓ Use both together: obj?.prop ?? default ✓ Chain from access to default naturally ✓ Handles deep nesting elegantly Avoid: ✗ Mixing ?? with && or || without () ✗ Using || when ?? is needed ✗ Forgetting null vs undefined semantics

Conclusion#

The nullish coalescing operator (??) provides precise control over default values by only triggering for null and undefined, unlike || which triggers for any falsy value. Use it when zero, empty strings, or false are valid values. Combine it with optional chaining (?.) for elegant handling of potentially missing nested properties. The ??= assignment operator adds convenient lazy initialization patterns.

Share this article

Help spread the word about Bootspring