undefined means a value was never assigned (missing property, missing argument, bare return). null is an intentional empty value you assign yourself. Prefer === when you care which one you have, use == null when either should count as empty, and reach for ?? / optional chaining when you want defaults without treating 0 or '' as missing.
MDN documents both primitives: null is the intentional absence of an object value, while undefined is the primitive for an uninitialized value. This guide shows the behaviour with real output from Node.js 24.22.1 running test/js/demo.mjs on this box. If loose vs strict equality still feels fuzzy, the null == undefined case below is one of the classic places where == and === disagree.
I also checked the same snippets mentally against everyday DOM and fetch patterns: document.getElementById returns null on a miss, optional JSON fields come back as undefined when absent, and many REST APIs send explicit null for cleared fields. Keeping those three stories straight saves a surprising number of production bugs.
In this guide:
- At a glance: null vs undefined
- typeof null is "object" (yes, really)
- == vs === between null and undefined
- Missing properties vs explicit null
- Default parameters: undefined triggers, null does not
- Optional chaining (?.) safely reads nested values
- Nullish coalescing (??) vs logical OR (||)
- JSON.stringify and JSON.parse quirks
- Function returns, void 0, and ReferenceError
- DOM, APIs and everyday pitfalls
- Practical checks and when to use each
- Frequently asked questions
At a glance: null vs undefined
| Question | null | undefined |
|---|---|---|
| What it means | Intentional empty | Not assigned / missing |
typeof | "object" (historical quirk) | "undefined" |
null == undefined | true | |
null === undefined | false | |
| Default params trigger? | No | Yes |
?? treats as missing? | Yes | Yes |
| ` | ` treats as missing? | |
| JSON | Serialized as null | Keys omitted / array holes become null |
| Typical source | You assigned it; APIs; DOM | Missing props, no return, sparse arrays |
Rule of thumb I use in application code: assign null when I want to say "I looked, there is nothing." Leave a property undefined (or omit it) when it was never set. Check with === null / === undefined when the difference matters, and with == null when either empty state should take the same branch.
typeof null is "object" (yes, really)
The first surprise every JavaScript developer hits:
console.log("typeof null:", typeof null); console.log("typeof undefined:", typeof undefined); typeof null: object typeof undefined: undefined typeof null === "object" is a decades-old language quirk. It does not mean null is a real object you can read properties from. Accessing a property on null or undefined throws - the crash covered in Cannot read properties of undefined.
Use typeof to detect undefined. Detect null with value === null, not with typeof.
== vs === between null and undefined
Loose equality treats them as matching. Strict equality does not:
console.log("null == undefined:", null == undefined); console.log("null === undefined:", null === undefined); console.log("null == null:", null == null); console.log("undefined == undefined:", undefined == undefined); console.log("null == 0:", null == 0); console.log("undefined == 0:", undefined == 0); console.log("null == false:", null == false); console.log("undefined == false:", undefined == false); console.log("Boolean(null):", Boolean(null)); console.log("Boolean(undefined):", Boolean(undefined)); console.log("!null:", !null, "!undefined:", !undefined); null == undefined: true null === undefined: false null == null: true undefined == undefined: true null == 0: false undefined == 0: false null == false: false undefined == false: false Boolean(null): false Boolean(undefined): false !null: true !undefined: true Both are falsy, so if (!value) collapses them with 0, '' and false. That is often too aggressive. The idiom if (value == null) catches only null and undefined - useful when zero should still count as a real value. For a deeper look at loose vs strict equality, see == vs === in JavaScript.
Missing properties vs explicit null
A property you never set is undefined. A property you set to null is present and empty:
const user = { name: "Ana" }; const userNull = { name: "Ana", email: null }; console.log("'email' in user:", "email" in user); console.log("user.email:", user.email); console.log("'email' in userNull:", "email" in userNull); console.log("userNull.email:", userNull.email); console.log("user.email === undefined:", user.email === undefined); console.log("userNull.email === null:", userNull.email === null); 'email' in user: false user.email: undefined 'email' in userNull: true userNull.email: null user.email === undefined: true userNull.email === null: true APIs often use null to mean "field exists but is empty" (no email on file) and omit the key - or leave it undefined after JSON.parse of a missing field - to mean "not provided". Pick one convention in your own code and stick to it. Object keys with an undefined value still show up in Object.keys if you assigned them explicitly:
const mixed = { a: null, b: undefined, c: 1 }; console.log("Object.keys(mixed):", Object.keys(mixed)); console.log("Object.values(mixed):", Object.values(mixed)); console.log("Object.entries(mixed):", Object.entries(mixed)); Object.keys(mixed): [ 'a', 'b', 'c' ] Object.values(mixed): [ null, undefined, 1 ] Object.entries(mixed): [ [ 'a', null ], [ 'b', undefined ], [ 'c', 1 ] ] Default parameters: undefined triggers, null does not
This is one of the most practical differences in day-to-day functions:
function greet(name = "guest") { return "hi " + name; } console.log("greet():", greet()); console.log("greet(undefined):", greet(undefined)); console.log("greet(null):", greet(null)); console.log("greet('Ada'):", greet("Ada")); greet(): hi guest greet(undefined): hi guest greet(null): hi null greet('Ada'): hi Ada Omitting the argument or passing undefined applies the default. Passing null does not - you get the string "null" after concatenation. If callers might send null from an API, normalize at the edge: name = name ?? "guest" inside the body, or reject null explicitly. Variables declared with let/const without an initializer are undefined until assigned; see var vs let vs const for the scoping side of that story.
Optional chaining (?.) safely reads nested values
Optional chaining short-circuits to undefined when it hits null or undefined, instead of throwing:
const payload = { user: { id: 7 } }; console.log("payload.user?.profile?.avatar:", payload.user?.profile?.avatar); console.log("payload.missing?.x:", payload.missing?.x); const maybeNull = null; console.log("maybeNull?.length:", maybeNull?.length); payload.user?.profile?.avatar: undefined payload.missing?.x: undefined maybeNull?.length: undefined That return value is always undefined on a miss - never null - even if an intermediate was null. MDN's page on optional chaining covers ?.() and ?.[] forms too. Pair it with ?? when you want a default after a safe read.
Nullish coalescing (??) vs logical OR (||)
?? only falls through for null and undefined. || falls through for every falsy value:
console.log("null ?? 'fallback':", null ?? "fallback"); console.log("undefined ?? 'fallback':", undefined ?? "fallback"); console.log("0 ?? 'fallback':", 0 ?? "fallback"); console.log("'' ?? 'fallback':", "" ?? "fallback"); console.log("false ?? 'fallback':", false ?? "fallback"); console.log("null || 'or':", null || "or"); console.log("0 || 'or':", 0 || "or"); null ?? 'fallback': fallback undefined ?? 'fallback': fallback 0 ?? 'fallback': 0 '' ?? 'fallback': false ?? 'fallback': false null || 'or': or 0 || 'or': or Use ?? when 0, '' or false are valid data. Use || only when any falsy value should become the default. MDN documents the operator under nullish coalescing.
JSON.stringify and JSON.parse quirks
JSON has null but no undefined. Serialization reflects that:
console.log("JSON.stringify({a:null,b:undefined}):", JSON.stringify({ a: null, b: undefined })); console.log("JSON.stringify([null, undefined]):", JSON.stringify([null, undefined])); console.log("JSON.parse('{\"a\":null}'):", JSON.parse('{"a":null}')); console.log("JSON.parse('{}').b:", JSON.parse("{}").b); JSON.stringify({a:null,b:undefined}): {"a":null} JSON.stringify([null, undefined]): [null,null] JSON.parse('{"a":null}'): { a: null } JSON.parse('{}').b: undefined Object keys whose value is undefined are dropped. Array holes / undefined slots become null in the JSON text. After JSON.parse, a missing key reads back as undefined, while an explicit JSON null stays null. MDN's JSON.stringify page covers replacer behaviour if you need custom handling.
Function returns, void 0, and ReferenceError
A function with no return yields undefined. Returning null is a deliberate choice:
function noReturn() {} function returnNull() { return null; } function returnUndef() { return undefined; } console.log("noReturn():", noReturn()); console.log("returnNull():", returnNull()); console.log("returnUndef():", returnUndef()); console.log("void 0 === undefined:", void 0 === undefined); let x; console.log("let x; x === undefined:", x === undefined); noReturn(): undefined returnNull(): null returnUndef(): undefined void 0 === undefined: true let x; x === undefined: true Reading a variable that was never declared is a ReferenceError, not undefined - except with typeof, which safely returns "undefined":
try { console.log(notDeclaredAtAll); } catch (e) { console.log("caught:", e.name + ": " + e.message); } console.log("typeof notDeclaredAtAll:", typeof notDeclaredAtAll); caught: ReferenceError: notDeclaredAtAll is not defined typeof notDeclaredAtAll: undefined Sparse arrays also produce undefined reads for empty slots, but in says the index is missing:
const arr = [1, , 3]; console.log("arr.length:", arr.length); console.log("arr[1]:", arr[1]); console.log("1 in arr:", 1 in arr); console.log("arr.map(x => x):", arr.map(x => x)); arr.length: 3 arr[1]: undefined 1 in arr: false arr.map(x => x): [ 1, <1 empty item>, 3 ] DOM, APIs and everyday pitfalls
Three places this distinction shows up constantly:
- DOM lookups -
document.getElementById('missing')returnsnull. Reading.valueon that result throws. Guard withif (el == null)before you touch properties. - Fetch / JSON APIs - a missing JSON key becomes
undefinedafterJSON.parse. An explicit"email": nullstaysnull. Do not treat them the same if your UI has different copy for "unknown" vs "cleared". - Function options objects -
fn({ timeout: undefined })still has the key; defaults written asoptions.timeout ?? 3000will replace it, which is usually what you want. Passingtimeout: nullalso triggers??, so document whethernullmeans "no timeout" or "use default".
Optional chaining helps for deep reads, but remember a miss always yields undefined, even when the chain hit a null. If you need to preserve an explicit null from an API as null in your UI state, assign it deliberately after the read instead of relying on ?. alone.
Array holes are another footgun: sparse slots read as undefined, yet in says the index is missing, and map skips the callback for empty slots. Prefer dense arrays (filter, flat, or explicit undefined fills) when you care about every index.
Practical checks and when to use each
Helpers I keep nearby:
function isNullish(v) { return v == null; } // catches null and undefined function isNull(v) { return v === null; } function isUndef(v) { return typeof v === "undefined"; } console.log("isNullish(null):", isNullish(null)); console.log("isNullish(undefined):", isNullish(undefined)); console.log("isNullish(0):", isNullish(0)); console.log("isNull(null):", isNull(null)); console.log("isUndef(undefined):", isUndef(undefined)); console.log("isUndef(null):", isUndef(null)); isNullish(null): true isNullish(undefined): true isNullish(0): false isNull(null): true isUndef(undefined): true isUndef(null): false - Use
undefinedfor "not set yet": missing optional args, uninitialized locals, absent object keys. - Use
nullfor "set, but empty": cleared form fields, API "no value", DOM nodes that do not exist (document.getElementByIdreturnsnull). - Check with
===when the difference matters;== nullwhen either empty state is fine. - Prefer
??over||for defaults when zero/empty-string are valid. - Prefer
?.over manual null checks for deep reads.
If you call something that is not a function while handling these values, you will see a different error - covered in TypeError: X is not a function.
Quick decision tree I use when reviewing a PR:
- Is the empty state intentional and persistent? Prefer
null. - Is the value simply not provided yet? Prefer leaving it
undefined/ omitted. - Do I need a default without wiping
0or''? Use??, not||. - Am I reading a deep path that might be missing? Use
?., then??for the default. - Am I comparing to empty? Prefer
== nullfor either, or===when the exact flavour matters.
All snippets above were run on Node.js 24.22.1 via test/js/demo.mjs. The same results hold in current Chromium and Firefox for the language-level bits (typeof, ??, ?., JSON.stringify). DOM return values are browser APIs, not Node, but the null convention is the same in the HTML Standard.
Frequently asked questions
What is the difference between null and undefined in JavaScript?
undefined means a value was never assigned (missing property, missing argument, no return). null is an intentional empty value you assign. They compare equal with == and unequal with ===.
Why is typeof null equal to "object"?
It is a long-standing language quirk. null is still a primitive, not a real object. Detect it with value === null, not with typeof.
When should I use == null?
When either null or undefined should take the same branch. value == null is true only for those two; it leaves 0, '' and false alone.
Do default parameters work with null?
No. Defaults apply when the argument is missing or undefined. Passing null uses null as the value.
What does ?? do that || does not?
?? falls back only for null and undefined. || falls back for every falsy value, including 0, '' and false.
How does JSON treat null and undefined?
JSON has null but not undefined. JSON.stringify drops object keys whose value is undefined, and turns undefined array slots into null. JSON.parse of a missing key yields undefined when you read it.
Is a missing object property null or undefined?
Reading a missing property returns undefined. The key is not in the object. An explicit null means the key exists and was set empty.
What does document.getElementById return when nothing matches?
It returns null, not undefined. That is a common DOM example of intentional emptiness, so check with === null or == null before you read properties.
Last checked: 7 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.
Originally published on mfaysal.com.
I'm learning cybersecurity on TryHackMe — follow my progress at tryhackme.com/p/mfaysal.dev.
