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 valueChaining#
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.