Back to Blog
JavaScriptOperatorsES2021Assignment

JavaScript Nullish Assignment Operators Guide

Master JavaScript nullish assignment operators (??=, ||=, &&=) for concise conditional assignments.

B
Bootspring Team
Engineering
November 7, 2019
6 min read

Logical assignment operators combine logical operations with assignment for cleaner code. Here's how to use them.

Nullish Coalescing Assignment (??=)#

1// Assign if null or undefined 2let x = null; 3x ??= 'default'; 4console.log(x); // 'default' 5 6let y = undefined; 7y ??= 'default'; 8console.log(y); // 'default' 9 10// Does NOT assign for falsy values 11let z = 0; 12z ??= 100; 13console.log(z); // 0 (not reassigned) 14 15let str = ''; 16str ??= 'fallback'; 17console.log(str); // '' (not reassigned) 18 19let bool = false; 20bool ??= true; 21console.log(bool); // false (not reassigned) 22 23// Equivalent to 24// x = x ?? 'default'; 25// if (x === null || x === undefined) x = 'default';

Logical OR Assignment (||=)#

1// Assign if falsy 2let x = 0; 3x ||= 100; 4console.log(x); // 100 (0 is falsy) 5 6let str = ''; 7str ||= 'default'; 8console.log(str); // 'default' ('' is falsy) 9 10let bool = false; 11bool ||= true; 12console.log(bool); // true (false is falsy) 13 14let y = null; 15y ||= 'fallback'; 16console.log(y); // 'fallback' 17 18// Doesn't assign for truthy values 19let z = 'hello'; 20z ||= 'default'; 21console.log(z); // 'hello' (not reassigned) 22 23// Equivalent to 24// x = x || 100; 25// if (!x) x = 100;

Logical AND Assignment (&&=)#

1// Assign if truthy 2let x = 'hello'; 3x &&= 'world'; 4console.log(x); // 'world' 5 6let y = 100; 7y &&= 200; 8console.log(y); // 200 9 10// Doesn't assign for falsy values 11let z = null; 12z &&= 'value'; 13console.log(z); // null (not reassigned) 14 15let num = 0; 16num &&= 100; 17console.log(num); // 0 (not reassigned) 18 19// Equivalent to 20// x = x && 'world'; 21// if (x) x = 'world';

Object Property Defaults#

1// Initialize missing properties 2const config = { 3 theme: 'dark', 4 // debug is missing 5}; 6 7config.debug ??= false; 8config.timeout ??= 5000; 9config.theme ??= 'light'; // Won't override 10 11console.log(config); 12// { theme: 'dark', debug: false, timeout: 5000 } 13 14// Nested properties 15const user = { 16 settings: {}, 17}; 18 19user.settings.notifications ??= true; 20user.settings.language ??= 'en'; 21 22// Compare with ||= for strings that might be empty 23const form = { 24 name: '', 25 email: 'user@example.com', 26}; 27 28form.name ||= 'Anonymous'; // Assigns (empty string is falsy) 29form.email ||= 'default@test'; // Doesn't assign 30 31form.name ??= 'Anonymous'; // Wouldn't assign ('' is not null)

Function Parameters#

1// Default parameter values 2function connect(options) { 3 options.host ??= 'localhost'; 4 options.port ??= 3000; 5 options.ssl ??= false; 6 7 return options; 8} 9 10connect({ host: 'api.example.com' }); 11// { host: 'api.example.com', port: 3000, ssl: false } 12 13// Mutable defaults 14function processData(data, options = {}) { 15 options.format ??= 'json'; 16 options.validate ??= true; 17 options.transform ??= (x) => x; 18 19 return options.transform(data); 20} 21 22// Cache initialization 23function getOrCompute(cache, key, compute) { 24 cache[key] ??= compute(); 25 return cache[key]; 26} 27 28const cache = {}; 29getOrCompute(cache, 'result', () => expensiveCalculation());

Lazy Initialization#

1// Initialize on first access 2class LazyLoader { 3 constructor() { 4 this._data = null; 5 } 6 7 get data() { 8 return (this._data ??= this.loadData()); 9 } 10 11 loadData() { 12 console.log('Loading data...'); 13 return { items: [] }; 14 } 15} 16 17const loader = new LazyLoader(); 18console.log(loader.data); // Loads on first access 19console.log(loader.data); // Uses cached value 20 21// Singleton pattern 22class Database { 23 static instance; 24 25 static getInstance() { 26 return (Database.instance ??= new Database()); 27 } 28} 29 30// Memoization 31function memoize(fn) { 32 const cache = {}; 33 34 return function (...args) { 35 const key = JSON.stringify(args); 36 return (cache[key] ??= fn.apply(this, args)); 37 }; 38}

Array Operations#

1// Initialize array if needed 2const groups = {}; 3 4function addToGroup(groupName, item) { 5 groups[groupName] ??= []; 6 groups[groupName].push(item); 7} 8 9addToGroup('users', 'Alice'); 10addToGroup('users', 'Bob'); 11addToGroup('admins', 'Charlie'); 12 13// { users: ['Alice', 'Bob'], admins: ['Charlie'] } 14 15// Counting occurrences 16const counts = {}; 17 18function count(item) { 19 counts[item] ??= 0; 20 counts[item]++; 21} 22 23['a', 'b', 'a', 'c', 'b', 'a'].forEach(count); 24// { a: 3, b: 2, c: 1 } 25 26// Better with ||= for numbers starting at 0 27const counts2 = {}; 28['a', 'b', 'a'].forEach((item) => { 29 counts2[item] ||= 0; // Doesn't work! 0 is falsy 30 counts2[item] ??= 0; // Works correctly 31 counts2[item]++; 32});

State Management#

1// Redux-like reducer 2function reducer(state, action) { 3 switch (action.type) { 4 case 'SET_USER': 5 return { 6 ...state, 7 user: action.payload, 8 }; 9 10 case 'INIT_SETTINGS': 11 // Only set if not already initialized 12 state.settings ??= {}; 13 state.settings.theme ??= 'light'; 14 state.settings.language ??= 'en'; 15 return { ...state }; 16 17 default: 18 return state; 19 } 20} 21 22// React state 23function useSettings(initial) { 24 const [settings, setSettings] = useState(() => { 25 const saved = localStorage.getItem('settings'); 26 const parsed = saved ? JSON.parse(saved) : {}; 27 28 parsed.theme ??= initial.theme ?? 'light'; 29 parsed.fontSize ??= initial.fontSize ?? 16; 30 31 return parsed; 32 }); 33 34 return [settings, setSettings]; 35}

DOM Manipulation#

1// Element data storage 2function setElementData(element, key, value) { 3 element._customData ??= {}; 4 element._customData[key] = value; 5} 6 7function getElementData(element, key, defaultValue) { 8 element._customData ??= {}; 9 element._customData[key] ??= defaultValue; 10 return element._customData[key]; 11} 12 13// Event handler registry 14const handlers = new WeakMap(); 15 16function addHandler(element, type, handler) { 17 let elementHandlers = handlers.get(element); 18 elementHandlers ??= {}; 19 elementHandlers[type] ??= []; 20 elementHandlers[type].push(handler); 21 handlers.set(element, elementHandlers); 22}

API Response Handling#

1// Normalize API response 2function normalizeUser(apiUser) { 3 const user = { ...apiUser }; 4 5 user.displayName ??= user.name ?? 'Anonymous'; 6 user.avatar ??= '/default-avatar.png'; 7 user.role ??= 'user'; 8 user.preferences ??= {}; 9 user.preferences.notifications ??= true; 10 11 return user; 12} 13 14// Default values for missing fields 15async function fetchUserData(userId) { 16 const response = await fetch(`/api/users/${userId}`); 17 const data = await response.json(); 18 19 data.createdAt ??= new Date().toISOString(); 20 data.updatedAt ??= data.createdAt; 21 data.status ??= 'active'; 22 23 return data; 24}

Comparison Table#

1// Summary of behaviors 2const value = null; 3 4// ??= only assigns for null/undefined 5value ??= 'default'; // Assigns 6 7// ||= assigns for any falsy value 8value ||= 'default'; // Assigns 9 10// &&= assigns for any truthy value 11value &&= 'other'; // Does NOT assign (null is falsy) 12 13// With 0 14let num = 0; 15num ??= 100; // num stays 0 16num ||= 100; // num becomes 100 17num &&= 100; // num stays 0 18 19// With '' 20let str = ''; 21str ??= 'x'; // str stays '' 22str ||= 'x'; // str becomes 'x' 23str &&= 'x'; // str stays '' 24 25// With false 26let bool = false; 27bool ??= true; // bool stays false 28bool ||= true; // bool becomes true 29bool &&= true; // bool stays false

Best Practices#

Usage: ✓ ??= for null/undefined defaults ✓ ||= for falsy defaults ✓ &&= for conditional updates ✓ Initialize object properties Patterns: ✓ Lazy initialization ✓ Cache population ✓ Default parameters ✓ Object property defaults Prefer ??= when: ✓ 0 is a valid value ✓ '' is a valid value ✓ false is a valid value ✓ Only null/undefined should trigger Avoid: ✗ Complex chaining ✗ Side effects in right-hand side ✗ Confusion with similar operators ✗ Overusing when simple = works

Conclusion#

Logical assignment operators provide concise syntax for conditional assignments. Use ??= when only null or undefined should trigger assignment, ||= when any falsy value should trigger, and &&= when you want to replace truthy values. They're particularly useful for setting defaults, lazy initialization, and normalizing data.

Share this article

Help spread the word about Bootspring