New to Rust? Grab our free Rust for Beginners eBook Get it free →
Null vs undefined in JavaScript: What the difference actually means

JavaScript uses null and undefined for different kinds of missing data. Every example below ran with Node.js 26.7.0 to show where the values come from, how each operator treats them, and which value survives a JSON request.
Null and undefined in one table
The shortest distinction concerns intent. Null is an explicit empty value, while undefined usually means JavaScript did not produce a value.
| Question | null | undefined |
|---|---|---|
| Who usually provides it? | Your code or an API | JavaScript when no value is produced |
| Primitive value? | Yes | Yes |
| typeof result | object | undefined |
| Strictly equal to the other? | No | No |
| Kept in a JSON object? | Yes | No |
| Triggers a default parameter? | No | Yes |
The object result from typeof null is a backward-compatibility bug, not proof that null is an object. Use strict equality when you need to identify null.
Where each value comes from
Both values can reach your program through more than one route. The source helps you decide whether the absence was expected, omitted, or explicitly recorded.
JavaScript produces undefined
An uninitialized variable, a missing object property, an omitted function argument, and a function without a returned value all produce undefined. The value says the operation did not supply a result.
Code and browser APIs can produce null
You can assign null to mark an intentionally empty field, and browser APIs can return it when document.querySelector() finds no element or String.match() finds no match.
Null is explicit in the API contract, but the call site does not always write the value itself.
import assert from 'node:assert/strict';
let pending;
const selected = null;
console.log('pending:', pending);
console.log('selected:', selected);
console.log('typeof pending:', typeof pending);
console.log('typeof selected:', typeof selected);
assert.equal(pending, undefined);
assert.equal(selected, null);
assert.equal(typeof pending, 'undefined');
assert.equal(typeof selected, 'object');
The assertions stop the process if either value or typeof result changes, while the variable names distinguish deliberate emptiness from a binding without an assignment.
Assigning null removes one reference without forcing garbage collection because another variable may still point to the same object.
Compare null and undefined safely
Use strict equality when the two states lead to different actions. It checks the values without converting either operand.
Strict equality keeps them separate
The describe() function returns separate labels for null and undefined, then leaves every other value in the present branch.
import assert from 'node:assert/strict';
function describe(value) {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
return 'present';
}
console.log(describe(null));
console.log(describe(undefined));
console.log(null === undefined);
console.log(null == undefined);
assert.equal(describe(null), 'null');
assert.equal(describe(undefined), 'undefined');
assert.equal(null === undefined, false);
assert.equal(null == undefined, true);
Because typeof cannot identify null on its own, a direct null check handles the legacy object result without mistaking an ordinary object for missing data.
One intentional use of loose equality
Loose equality is usually broader than you need, but value == null has one narrow behavior. It matches only null and undefined among the common falsy values, so it can guard a boundary that treats both as absent.
Use that check only when both states trigger the same action, since strict checks preserve a contract where null clears a field and undefined leaves it unchanged.
The JavaScript operators reference explains the wider coercion rules behind loose and strict equality.
Defaults treat null and undefined differently
A default parameter runs for an omitted argument or an undefined value, while null bypasses the default because it is already supplied.
import assert from 'node:assert/strict';
function label(name = 'Guest') {
return name;
}
console.log(label());
console.log(label(undefined));
console.log(label(null));
console.log(null ?? 'Guest');
console.log('' ?? 'Guest');
assert.equal(label(), 'Guest');
assert.equal(label(undefined), 'Guest');
assert.equal(label(null), null);
assert.equal(null ?? 'Guest', 'Guest');
assert.equal('' ?? 'Guest', '');
Unlike a default parameter, the nullish coalescing operator replaces either null or undefined while preserving an empty string, zero, and false.
The logical OR operator would replace those valid falsy values along with missing ones.
JSON preserves null but omits undefined object properties
JSON has a null value but no undefined value. JSON.stringify() therefore keeps null object properties and omits object properties whose value is undefined.
import assert from 'node:assert/strict';
const objectJson = JSON.stringify({
nickname: undefined,
bio: null,
});
const arrayJson = JSON.stringify([undefined, null]);
console.log(objectJson);
console.log(arrayJson);
assert.equal(objectJson, '{"bio":null}');
assert.equal(arrayJson, '[null,null]');
In an array, an undefined element becomes null so the serialized array keeps its positions.
This affects update requests. Null can mean clear this field, while an omitted property can mean leave this field unchanged, but the server contract must define that meaning.
Optional chaining returns undefined
Optional chaining stops when its left side is null or undefined and returns undefined for the whole chain. It does not preserve null as the result of the failed access.
The same example checks numeric conversion because null converts to zero in numeric addition, while undefined produces Not a Number (NaN).
import assert from 'node:assert/strict';
const account = null;
const city = account?.profile?.city;
const nullTotal = 10 + null;
const undefinedTotal = 10 + undefined;
console.log('city:', city);
console.log('10 + null:', nullTotal);
console.log('10 + undefined:', undefinedTotal);
assert.equal(city, undefined);
assert.equal(nullTotal, 10);
assert.equal(Number.isNaN(undefinedTotal), true);
Neither conversion validates your input, so check the value before arithmetic when replacing missing data with zero would hide an incomplete form or API response.
Run every behavior together
Save the next file as null-vs-undefined.js when you want one repeatable check. The object records each observed result, and deepEqual stops execution if any value differs from the expected state.
import assert from 'node:assert/strict';
let pending;
const selected = null;
const objectJson = JSON.stringify({ nickname: undefined, bio: null });
const arrayJson = JSON.stringify([undefined, null]);
const city = selected?.profile?.city;
const observations = {
'typeof undefined': typeof pending,
'typeof null': typeof selected,
'null === undefined': null === undefined,
'null == undefined': null == undefined,
'undefined ?? Guest': pending ?? 'Guest',
'null ?? Guest': selected ?? 'Guest',
'JSON object': objectJson,
'JSON array': arrayJson,
'optional chain': city,
'10 + null': 10 + null,
'10 + undefined is NaN': Number.isNaN(10 + undefined),
};
for (const [name, value] of Object.entries(observations)) {
console.log(`${name}: ${String(value)}`);
}
assert.deepEqual(observations, {
'typeof undefined': 'undefined',
'typeof null': 'object',
'null === undefined': false,
'null == undefined': true,
'undefined ?? Guest': 'Guest',
'null ?? Guest': 'Guest',
'JSON object': '{"bio":null}',
'JSON array': '[null,null]',
'optional chain': undefined,
'10 + null': 10,
'10 + undefined is NaN': true,
});
console.log('All checks passed.');
Run the file with the command shown below and confirm that it ends with All checks passed and exit status zero.

The output also shows why typeof alone is incomplete. The direct equality checks, JSON results, optional-chain result, and arithmetic results establish the decision in context.
Which value should you use?
Use null when your contract needs an explicit empty value that survives serialization, and let undefined represent an omitted property, an uninitialized binding, or an operation without a returned value.
At an API boundary, you may accept both when they mean the same thing. Normalize them once, then keep one internal convention so later checks do not have to guess.
Use strict equality when the states have different meanings, nullish coalescing when both receive the same fallback, and explicit validation before arithmetic.
The JavaScript type-checking guide is the useful next step when arrays, dates, and custom objects join the same input path.




