Have you heard about the nullish coalescing operator (??) ? Can you tell me about it?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Easy
#JavaScript #JS Basics #Operators

Quick Answer

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.


What is the Nullish Coalescing Operator

Syntax and Basic Usage

// Basic syntax
const result = leftValue ?? rightValue;
 
// Examples
const userName = user.name ?? "Anonymous user";
const userAge = user.age ?? 0;
const userSettings = user.settings ?? {};

When Does ?? Trigger?

The ?? operator returns the right operand only if the left equals:

  • null
  • undefined
// 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

Comparison of ?? and ||

Value?? “default”|| “default”Explanation
null"default""default"Both operators replace
undefined"default""default"Both operators replace
00"default"?? preserves, || replaces
"""""default"?? preserves, || replaces
falsefalse"default"?? preserves, || replaces
NaNNaN"default"?? preserves, || replaces
"text""text""text"Both preserve
424242Both preserve

Key Differences

// 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)

Practical Examples

1. User Settings

// ❌ 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!

2. API Responses

// 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 }

3. Application Configuration

// 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

Chaining ?? Operators

Multiple Checks

// 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";
}

Combining with Optional Chaining

// 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";

Nullish Coalescing Assignment (??=)

Assignment with Check

// 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"

Practical Application of ??=

// 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 cache

Practical Tasks

Task 1: What will the console output?

const 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");
Answer

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)

Task 2: Fix the function

// ❌ 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?
Answer

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 }

Task 3: Operator Chain

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); // ?
Answer

"" (empty string)

  • twitter equals null → proceed to next
  • facebook equals undefined → proceed to next
  • instagram equals "" → return "" (empty string is not equal to null/undefined)

Task 4: Operator ??=

const settings = {
  theme: "dark",
  language: null,
  notifications: undefined
};
 
settings.theme ??= "light";
settings.language ??= "en";
settings.notifications ??= true;
settings.newFeature ??= "enabled";
 
console.log(settings);
Answer
{
  theme: "dark",           // Didn't change (already had value)
  language: "en",          // Changed (was null)
  notifications: true,     // Changed (was undefined)
  newFeature: "enabled"    // Added (didn't exist)
}

Task 5: Combining with Optional Chaining

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);
Answer

["Anna", "Name not specified", "Name not specified"]

  • First user: profile.name exists → "Anna"
  • Second user: profile equals null?.name returns undefined?? returns "Name not specified"
  • Third user: profile doesn’t exist → ?.name returns undefined?? returns "Name not specified"

Best Practices

1. Use ?? for null/undefined

// ✅ 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

2. Combine with Optional Chaining

// ✅ 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";

3. Use ??= for Initialization

// ✅ Good: property initialization
config.timeout ??= 5000;
config.retries ??= 3;
 
// ❌ Bad: redundant checks
if (config.timeout === null || config.timeout === undefined) {
  config.timeout = 5000;
}

4. Document Intentions

// ✅ 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 };
}

Browser Support

Compatibility

  • Chrome: 80+
  • Firefox: 72+
  • Safari: 13.1+
  • Edge: 80+
  • Node.js: 14+

Polyfill for Older Browsers

// 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

Modern Features

Logical Assignment Operators (ES2021)

// 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"

Combining with Other Operators

// 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;

Common Mistakes

1. Confusion with || Operator

// ❌ 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;
}

2. Incorrect Use with Boolean

// ❌ 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
}

3. Redundant Checks

// ❌ Bad: redundant
const value = (data !== null && data !== undefined) ? data : "default";
 
// ✅ Good: concise
const value = data ?? "default";

Conclusion

Golden Rules:

  1. Use ?? for checking null/undefined — it’s precise and predictable
  2. Use || for checking “falsy” values — when you need to replace 0, "", false
  3. Combine with Optional Chaining (?.) — for safe property access
  4. Use ??= for initialization — concise and readable
  5. Document intentions — make code clear for other developers

Remember: 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 💪