Both values represent the absence of a meaningful value. But they come from different sources, mean different things, and need to be handled differently. Treating them as interchangeable leads to subtle bugs and APIs that are hard to use correctly.
The Semantic Difference
undefined — the engine assigned this. A variable was declared but never assigned. An object property was accessed that doesn't exist. A function was called without an expected argument. A function returned without a value. In all of these cases, JavaScript itself set the value to undefined — you didn't.
null — a developer assigned this. Explicitly setting a value to null is a programmer saying "this is intentionally empty." It's a deliberate signal, not an automatic one.
let user // undefined — declared, never assigned (engine's doing)
let count = null // null — explicitly set to empty (developer's doing)
This is the distinction that matters for API design: if a value is undefined, something probably wasn't provided. If a value is null, a developer intentionally marked it as absent.
When undefined Appears Automatically
Uninitialized variables:
let name console.log(name) // undefined
Missing function arguments:
function greet(name) { console.log(name) // undefined if called as greet() } greet() // undefined
Missing object properties:
const user = { name: 'Alice' } console.log(user.age) // undefined — property doesn't exist console.log(user.address?.city) // undefined — optional chaining
Functions with no return:
function doSomething() { // no return statement } const result = doSomething() // undefined
Array holes:
const arr = [1, , 3] // sparse array arr[1] // undefined — slot exists, no value assigned
When to Use null
Use null when you are deliberately signaling absence — especially in places where undefined would be ambiguous:
// Database record: user has no profile photo yet (intentional, not missing)
const user = {
name: 'Alice',
avatar: null, // deliberately empty
bio: null, // deliberately empty
}
// API response: field exists but has no value const response = { data: { user: null }, // user was fetched, but doesn't exist error: null, // no error occurred }
// Clearing a ref in React const ref = useRef(null) // After the component unmounts, the ref is set back to null by React
A useful pattern: resetting vs never-setting
// undefined = never provided this option
// null = explicitly clearing the option
function updateUser(id, updates) { // 'name' === undefined → don't change the name // 'name' === null → explicitly clear the name // 'name' === 'Alice' → set name to 'Alice'
if (updates.name !== undefined) { db.users.update(id, { name: updates.name }) // includes null → clears name } }
typeof: The Bug Hidden in Plain Sight
typeof undefined // 'undefined'
typeof null // 'object' ← not 'null'
typeof null === 'object' is a bug from JavaScript's original 1995 implementation. In the original engine, values were stored with a type tag, and null's type tag happened to be the same as objects. The bug was never fixed because fixing it would break existing code.
This is why you should never check for null with typeof:
// ❌ Wrong — typeof null is 'object', not 'null'
if (typeof value === 'object') {
// This runs for null too!
}
// ✓ Correct null check if (value === null) { }
// ✓ Correct null OR undefined check (covers both) if (value == null) { } // == null catches both if (value === null || value === undefined) { } // explicit
Equality Comparison
null == undefined // true — spec defines this explicitly
null === undefined // false — different types (Null vs Undefined)
null == null // true null == 0 // false null == '' // false null == false // false
undefined == undefined // true undefined == 0 // false undefined == '' // false undefined == false // false
null == undefined being true with loose equality is one of the few == behaviors worth knowing. value == null is a concise way to check for either.
Nullish Coalescing vs OR — The Critical Difference
const port = config.port ?? 3000 // ?? — uses 3000 only if port is null or undefined
const port = config.port || 3000 // || — uses 3000 if port is ANY falsy value
// The difference matters when 0 is a valid value: config.port = 0
config.port ?? 3000 // 0 — correct, 0 is a valid port config.port || 3000 // 3000 — wrong, 0 is falsy so it falls through
Use ?? when null/undefined specifically means "no value provided." Use || only when you want to replace any falsy value — empty string, 0, false included.
Optional Chaining and undefined
Optional chaining (?.) returns undefined when a chain step is null or undefined:
const user = null
user?.address?.city // undefined — not an error user?.getName?.() // undefined — not an error
// Combine with ?? for defaults: user?.address?.city ?? 'Unknown' // 'Unknown'
Notice: optional chaining returns undefined, not null. Keep that in mind if you're checking what ?. produces.
Function Parameters: undefined vs null as Arguments
function process(value = 'default') {
return value
}
process() // 'default' — no argument = undefined = triggers default process(undefined) // 'default' — explicit undefined also triggers default process(null) // null — null does NOT trigger the default parameter
Default parameters only trigger for undefined, not for null. If you want a default for both, you need ??:
function process(input) {
const value = input ?? 'default' // 'default' for both null and undefined
return value
}
process(null) // 'default' process(undefined) // 'default' process('') // '' — empty string is not null/undefined
JSON Serialization Difference
const obj = {
a: undefined,
b: null,
c: 1,
}
JSON.stringify(obj) // '{"b":null,"c":1}' // undefined properties are omitted entirely from JSON output // null is serialized as the JSON null literal
If you're sending data to an API, null and undefined behave completely differently. An object with undefined values will lose those fields in JSON serialization. Use null explicitly when you want the field to appear in the payload with an empty value.
The Practical Decision Rule
- Use
undefinedimplicitly — let it appear naturally from uninitialized variables, missing args, missing properties. Don't assignundefinedmanually; that's whatnullis for.
- Use
nullexplicitly — when you deliberately want to mark a value as absent in objects, database records, or API payloads.
- Check for both with
value == nullorvalue ?? fallback.
- Never use
typeof value === 'object'as a null check.
- Remember default parameters don't fire for
null— only forundefined.