JavaScript Basics

JavaScript null vs undefined: Understanding the Differences

Learn the key differences between null and undefined in JavaScript. Understand when to use each, common pitfalls, and best practices for handling these values.

By JavaScript Document Team
nullundefinedtypesbasicsfundamentals

JavaScript has two distinct values for "nothing": null and undefined. While they might seem similar, they have different meanings and uses. Understanding these differences is crucial for writing robust JavaScript code.

What is undefined?

undefined means a variable has been declared but has not been assigned a value. It's JavaScript's way of saying "this doesn't have a value yet."

When You Encounter undefined

// 1. Declared but not initialized variables
let x;
console.log(x); // undefined

// 2. Object properties that don't exist
const person = { name: 'John' };
console.log(person.age); // undefined

// 3. Function parameters not provided
function greet(name) {
  console.log(name); // undefined if called without arguments
}
greet();

// 4. Functions without return statements
function doSomething() {
  // No return statement
}
console.log(doSomething()); // undefined

// 5. Array elements that don't exist
const arr = [1, 2, 3];
console.log(arr[10]); // undefined

// 6. Destructuring non-existent values
const { missing } = {};
console.log(missing); // undefined

typeof undefined

console.log(typeof undefined); // 'undefined'

// Checking for undefined
let value;

// Method 1: Direct comparison
if (value === undefined) {
  console.log('Value is undefined');
}

// Method 2: typeof check (safer)
if (typeof value === 'undefined') {
  console.log('Value is undefined');
}

// Method 3: Void operator
if (value === void 0) {
  console.log('Value is undefined');
}

What is null?

null represents an intentional absence of any object value. It's explicitly set by developers to indicate "no value" or "empty."

When to Use null

// 1. Explicitly indicating no value
let selectedUser = null; // No user selected

// 2. Clearing object references
let data = { name: 'John' };
data = null; // Clear the reference

// 3. As a placeholder for objects
class User {
  constructor() {
    this.profile = null; // Will be set later
  }

  loadProfile() {
    // Load profile data
    this.profile = { name: 'John', age: 30 };
  }
}

// 4. Default parameter values
function processData(data = null) {
  if (data === null) {
    console.log('No data provided');
    return;
  }
  // Process data
}

// 5. Resetting values
let connection = establishConnection();
// Later...
connection = null; // Reset connection

typeof null

// Historical bug: typeof null returns 'object'
console.log(typeof null); // 'object' (not 'null'!)

// Proper null checking
let value = null;

// Direct comparison
if (value === null) {
  console.log('Value is null');
}

// Checking for null or undefined
if (value == null) {
  console.log('Value is null or undefined');
}

Key Differences

1. Meaning and Intent

// undefined - JavaScript's default "no value"
let uninitialized;
console.log(uninitialized); // undefined (automatic)

// null - Developer's intentional "no value"
let intentionallyEmpty = null; // Explicitly set

// Example: User authentication
let currentUser; // undefined - we haven't checked yet
// After checking...
currentUser = null; // Explicitly no user logged in
// Or...
currentUser = { id: 1, name: 'John' }; // User found

2. Type Differences

// Different types
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object' (historical bug)

// Type checking
function getType(value) {
  if (value === null) return 'null';
  if (value === undefined) return 'undefined';
  return typeof value;
}

console.log(getType(undefined)); // 'undefined'
console.log(getType(null)); // 'null'

3. Default Parameter Behavior

// undefined triggers default parameters
function greet(name = 'Guest') {
  console.log(`Hello, ${name}!`);
}

greet(); // 'Hello, Guest!' (undefined triggers default)
greet(undefined); // 'Hello, Guest!' (undefined triggers default)
greet(null); // 'Hello, null!' (null does NOT trigger default)
greet(''); // 'Hello, !' (empty string does NOT trigger default)

// Practical example
function createUser(name, age = 18, role = null) {
  return { name, age, role };
}

console.log(createUser('John')); // { name: 'John', age: 18, role: null }
console.log(createUser('Jane', undefined)); // { name: 'Jane', age: 18, role: null }
console.log(createUser('Bob', null)); // { name: 'Bob', age: null, role: null }

4. JSON Serialization

const obj = {
  a: undefined,
  b: null,
  c: 'value',
};

console.log(JSON.stringify(obj));
// '{"b":null,"c":"value"}' - undefined is omitted!

// Arrays behave differently
const arr = [1, undefined, null, 4];
console.log(JSON.stringify(arr));
// '[1,null,null,4]' - undefined becomes null in arrays

// Function to preserve undefined in JSON
function stringifyWithUndefined(obj) {
  return JSON.stringify(obj, (key, value) =>
    value === undefined ? '__undefined__' : value
  );
}

function parseWithUndefined(json) {
  return JSON.parse(json, (key, value) =>
    value === '__undefined__' ? undefined : value
  );
}

Equality Comparisons

Loose vs Strict Equality

// Loose equality (==)
console.log(null == undefined); // true
console.log(null == null); // true
console.log(undefined == undefined); // true

// Strict equality (===)
console.log(null === undefined); // false
console.log(null === null); // true
console.log(undefined === undefined); // true

// With other values
console.log(null == 0); // false
console.log(null == ''); // false
console.log(null == false); // false
console.log(undefined == 0); // false
console.log(undefined == ''); // false
console.log(undefined == false); // false

// Only null and undefined are loosely equal to each other
console.log(null == undefined); // true
console.log(undefined == null); // true

Checking for null or undefined

// Method 1: Loose equality with null
if (value == null) {
  // Catches both null and undefined
}

// Method 2: Explicit checks
if (value === null || value === undefined) {
  // More explicit, same result
}

// Method 3: Nullish coalescing operator (??)
const result = value ?? 'default';
// Uses 'default' if value is null or undefined

// Method 4: Optional chaining
const name = user?.profile?.name;
// Returns undefined if any part is null or undefined

Common Patterns and Best Practices

1. Initializing Variables

// Use null for objects that will be assigned later
let userData = null;
let connection = null;

// Let undefined be the default for uninitialized variables
let total; // undefined by default
let count; // undefined by default

// Use meaningful defaults when appropriate
let items = [];
let options = {};
let isEnabled = false;

2. Function Returns

// Return undefined for "not found" or "no result"
function findUser(id) {
  const user = database.find((u) => u.id === id);
  return user; // undefined if not found
}

// Return null for "empty" or "cleared" state
function getCurrentUser() {
  if (!isLoggedIn) {
    return null; // Explicitly no user
  }
  return userObject;
}

// Be consistent with return types
class Cache {
  get(key) {
    if (!this.has(key)) {
      return undefined; // Key doesn't exist
    }
    const value = this.store[key];
    return value === undefined ? null : value; // Stored undefined as null
  }

  set(key, value) {
    this.store[key] = value === undefined ? null : value;
  }
}

3. API Design

// Use null in JSON APIs
const apiResponse = {
  user: {
    id: 1,
    name: 'John',
    email: null, // Not undefined (won't serialize)
    phone: null, // Explicitly no phone
  },
};

// Configuration objects
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  cache: null, // Explicitly no cache
  headers: {}, // Empty object, not null or undefined
};

// Optional parameters
function createRequest(url, options = {}) {
  const {
    method = 'GET',
    headers = {},
    body = null, // Explicitly no body
    cache = undefined, // Use browser default
  } = options;

  // Build request...
}

4. Guard Clauses

// Checking for null or undefined
function processUser(user) {
  // Guard clause
  if (user == null) {
    throw new Error('User is required');
  }

  // Process user...
}

// Multiple checks
function updateProfile(userId, updates) {
  if (userId == null) {
    throw new Error('User ID is required');
  }

  if (updates == null || Object.keys(updates).length === 0) {
    throw new Error('Updates are required');
  }

  // Update profile...
}

// Using optional chaining and nullish coalescing
function getDisplayName(user) {
  return user?.profile?.displayName ?? user?.name ?? 'Anonymous';
}

Common Pitfalls

1. The typeof null Bug

// Wrong way
if (typeof value === 'object') {
  // This is true for null too!
  value.property; // Error if value is null
}

// Right way
if (value !== null && typeof value === 'object') {
  value.property; // Safe
}

// Or use optional chaining
value?.property; // Safe for null and undefined

2. Incorrect Default Values

// Problem: || operator checks for all falsy values
function getPort(config) {
  return config.port || 3000; // Wrong if port is 0
}

// Solution: Use nullish coalescing
function getPort(config) {
  return config.port ?? 3000; // Only defaults for null/undefined
}

// Problem with destructuring
const { timeout = 5000 } = options;
// timeout is 5000 only if undefined, not if null

// Solution: Handle null explicitly
const timeout = options.timeout ?? 5000;

3. Array Methods and undefined

const arr = [1, undefined, 3, null, 5];

// forEach skips empty slots but not undefined
arr.forEach((val) => console.log(val));
// 1, undefined, 3, null, 5

// map preserves undefined
const mapped = arr.map((x) => x * 2);
console.log(mapped); // [2, NaN, 6, 0, 10]

// filter can remove null and undefined
const filtered = arr.filter((x) => x != null);
console.log(filtered); // [1, 3, 5]

// Array with holes vs undefined
const sparse = [1, , 3]; // Hole at index 1
const withUndefined = [1, undefined, 3];

console.log(0 in sparse); // true
console.log(1 in sparse); // false (hole)
console.log(1 in withUndefined); // true

Utility Functions

// Check if value is null or undefined
function isNullish(value) {
  return value == null;
}

// Check if value is defined (not undefined)
function isDefined(value) {
  return value !== undefined;
}

// Check if value is set (not null or undefined)
function isSet(value) {
  return value != null;
}

// Safe property access
function getProperty(obj, path, defaultValue = undefined) {
  if (obj == null) return defaultValue;

  const keys = path.split('.');
  let result = obj;

  for (const key of keys) {
    result = result[key];
    if (result == null) return defaultValue;
  }

  return result;
}

// Usage
const user = { profile: { name: 'John' } };
console.log(getProperty(user, 'profile.name')); // 'John'
console.log(getProperty(user, 'profile.age')); // undefined
console.log(getProperty(user, 'settings.theme', 'light')); // 'light'

// Safe function call
function safeCall(fn, ...args) {
  if (typeof fn === 'function') {
    return fn(...args);
  }
  return undefined;
}

// Coalesce function
function coalesce(...values) {
  for (const value of values) {
    if (value != null) return value;
  }
  return undefined;
}

// Usage
const result = coalesce(null, undefined, '', 0, 'found');
console.log(result); // '' (first non-nullish value)

Best Practices Summary

  1. Use undefined for uninitialized variables and missing properties
  2. Use null for intentionally empty values and cleared references
  3. Use strict equality (===) for specific checks
  4. Use loose equality (== null) to check for both null and undefined
  5. Use nullish coalescing (??) for default values
  6. Use optional chaining (?.) for safe property access
  7. Be consistent in your codebase about when to use each
  8. Document your choices when the distinction matters

Conclusion

While null and undefined are both used to represent "no value" in JavaScript, they serve different purposes. undefined is JavaScript's default value for uninitialized variables, while null is used by developers to explicitly indicate emptiness. Understanding their differences and appropriate use cases helps write clearer, more intentional code and avoid common pitfalls.