null vs undefined — 'Cannot read property of null' Crash
Production outage: user.profile.name crashed on null in production.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- null and undefined both represent 'no value' but come from different sources.
- undefined is JavaScript's automatic default for uninitialized variables, missing properties, and absent returns.
- null is a programmer-assigned signal: 'I explicitly set this to nothing'.
- Performance: nullish coalescing (??) is safer than || when 0, '', or false are valid values.
- Production insight: Default function parameters only catch undefined, not null — leading to silent 'null' strings in output.
- Biggest mistake: typeof null returns 'object' — never use typeof to check for null.
In JavaScript, null and undefined are two distinct primitive values that both represent 'nothing' — but they mean very different things, and confusing them causes real production crashes. undefined is the default state JavaScript assigns to variables that have been declared but not initialized, function parameters that weren't provided, and missing object properties. It's the language's way of saying 'this thing doesn't exist yet.' null, on the other hand, is an intentional value you assign to explicitly indicate 'this thing exists but is empty or absent.' You create undefined by omission; you create null by assignment.
This distinction matters because null is a valid, serializable value in JSON, while undefined is silently stripped from objects during J — a fact that breaks APIs and database writes daily.SON.stringify()
The crash you're fixing — 'Cannot read property of null' — happens because JavaScript's type coercion and loose equality (==) treat null and undefined as equal, luring you into thinking they're interchangeable. They aren't. null is an object type (a historical bug now enshrined in the spec), while undefined is its own type.
When you access a property on null, you get a TypeError; on undefined, you get undefined back (which then fails if you chain another property access). The fix isn't just checking for one — it's understanding which one your code produces and handling both with explicit checks like x == null (which catches both) or x === null (which catches only null).
Real-world patterns like optional chaining (?.), nullish coalescing (??), and default parameters exist specifically to manage this duality without crashing.
In the ecosystem, TypeScript forces you to confront this distinction with strict null checks, and linting rules like no-unused-vars catch undefined declarations before they cause harm. Libraries like Lodash provide _.isNil() to check both, while frameworks like React treat null as a valid renderable value and undefined as a no-op.
The rule of thumb: use null when you want to explicitly clear a value (e.g., resetting a user's avatar to 'no image'), and let undefined remain the default for uninitialized state. Never assign undefined manually — that's the language's job. When you see 'Cannot read property of null', you're almost certainly dealing with an API response, a DOM query (document.querySelector returns null when no match), or a function that returned null to signal absence, and your code assumed it would always get an object.
Imagine you order a package online. 'undefined' is like checking your doorstep before the delivery truck has even left the warehouse — the package doesn't exist in your world yet, nobody told you anything about it. 'null' is like a delivery driver showing up and handing you an empty box on purpose — someone made a deliberate decision that nothing goes here. Both mean 'no value', but one is an accident of timing and the other is an intentional choice.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Every JavaScript program you'll ever write will eventually bump into null and undefined. They look similar on the surface — both seem to mean 'nothing' — but treating them as the same thing causes some of the most mysterious bugs you'll encounter as a developer. A form field that should be blank, a user profile that hasn't loaded yet, a database record that genuinely has no value — all of these map to slightly different meanings in code, and JavaScript gives you two distinct tools to express them.
The problem is that JavaScript itself uses undefined automatically behind the scenes, and developers often reach for null without fully understanding when that's the right call. Mixing them up leads to buggy equality checks, confusing API responses, and code that works 90% of the time and fails in ways that are genuinely hard to debug.
By the end of this article you'll know exactly what each value represents, where JavaScript creates undefined without you asking it to, how to write equality checks that don't lie to you, and when to intentionally use null in your own code. You'll also have the answers to the interview questions that trip up candidates who only half-understand this topic.
What 'undefined' Actually Means — and Who Creates It
The key insight about undefined is this: you almost never write it yourself. JavaScript creates it automatically whenever you try to access something that hasn't been given a value yet.
Declare a variable without assigning it? JavaScript sets it to undefined. Call a function that doesn't explicitly return anything? It returns undefined. Access an object property that doesn't exist? You get undefined back. Ask for the 10th item in a 3-item array? undefined.
Think of undefined as JavaScript's way of shrugging and saying 'I have no information about this.' It's the system's default — a placeholder that means 'this slot exists, but nobody put anything in it yet.'
This is why undefined is generally not something you should assign deliberately in your own code. If you write let userName = undefined, you're basically doing JavaScript's job for it in a confusing way. When you want to express 'no value', that's what null is for — which we'll get to next.
Understanding where JavaScript produces undefined on its own is crucial because it's the source of many 'cannot read properties of undefined' errors that beginners dread.
// ── CASE 1: Variable declared but never assigned ────────────────────────── let userAge; // JavaScript quietly sets this to undefined console.log('Declared but unassigned:', userAge); // undefined // ── CASE 2: Accessing an object property that does not exist ────────────── const userProfile = { firstName: 'Maria', lastName: 'Santos' }; // We never defined an email property — JavaScript returns undefined for it console.log('Missing property:', userProfile.email); // undefined // ── CASE 3: A function with no return statement ─────────────────────────── function greetUser(name) { // This function does work but forgets to return a value const message = `Hello, ${name}!`; // <-- no return statement here } const greeting = greetUser('Carlos'); console.log('No-return function result:', greeting); // undefined // ── CASE 4: Accessing an array index that doesn't exist ─────────────────── const colors = ['red', 'green', 'blue']; // indexes 0, 1, 2 exist console.log('Out-of-bounds index:', colors[10]); // undefined // ── CASE 5: A function parameter that was never passed ──────────────────── function displayScore(playerName, score) { // If score is not passed, JavaScript sets it to undefined automatically console.log(`${playerName} scored: ${score}`); } displayScore('Alex'); // score is never passed in
userProfile.address.street but userProfile.address was undefined — JavaScript couldn't go one level deeper. Always check the outer value before drilling into a nested property.What 'null' Actually Means — and When YOU Should Use It
null is a deliberate, programmer-assigned value. It means: 'I know this variable exists, I know it should hold a value eventually, and right now I am explicitly saying there is nothing here.'
This is a crucial distinction. undefined is what JavaScript gives you when it doesn't know. null is what you give JavaScript when you do know — specifically, when you know the answer is 'nothing'.
A real-world example: imagine you're building a user profile page. Before the user logs in, currentUser should be null — you're not waiting for data to load, you're consciously saying 'there is no logged-in user right now.' The moment they log in, you set currentUser to their actual data. When they log out, you set it back to null.
Another example: a database might store a user's middle name field as null because the user doesn't have a middle name. That's different from the field never being sent in the API response at all (which would likely give you undefined).
null is also the value you'll set variables to when you want to release memory — setting a large object to null tells the JavaScript engine's garbage collector that the memory can be reclaimed.
The short rule: you write null, JavaScript writes undefined.
// ── SCENARIO: A user authentication system ─────────────────────────────── // Before login: we deliberately set currentUser to null. // This means 'no user is logged in right now' — an intentional empty state. let currentUser = null; console.log('Before login, currentUser:', currentUser); // null // A function that simulates fetching a user from a database function findUserById(userId) { const database = [ { id: 1, name: 'Priya Nair', role: 'admin' }, { id: 2, name: 'Jake Thompson', role: 'viewer' } ]; const foundUser = database.find(user => user.id === userId); // If the user does not exist in the database, we return null deliberately. // This tells the caller: 'I looked — there is genuinely nobody here.' return foundUser || null; } // User 1 exists currentUser = findUserById(1); console.log('After finding user 1:', currentUser); // { id: 1, name: 'Priya Nair', role: 'admin' } // User 99 does not exist — we get null back intentionally const missingUser = findUserById(99); console.log('Looking for user 99:', missingUser); // null // ── Checking for null in a real condition ───────────────────────────────── if (missingUser === null) { console.log('No user found — show a 404 page or an error message.'); } // ── Setting to null on logout ───────────────────────────────────────────── function logout() { currentUser = null; // Deliberately clearing the value on logout console.log('User logged out. currentUser is now:', currentUser); } logout();
JSON Serialization: undefined is Omitted, null is Preserved
One of the most practical differences between null and undefined appears when you serialize JavaScript objects to JSON. The JSON.stringify() method treats them very differently: properties with a value of undefined are removed from the output entirely, while properties with null are included with the literal JSON null.
This distinction is critical for API design. If your backend returns an object with an optional field that is undefined, that field simply won't appear in the JSON response. But if the same field is null, it will appear as "fieldName": null. Client code that expects the field to always exist will crash if it gets undefined, but can safely check for null.
For this reason, many teams standardize on using null for all 'no value' cases in API responses. Omitting fields entirely can lead to inconsistencies and bugs when clients iterate over keys or check for property existence. Using null ensures the shape of the response is predictable.
When consuming an API, you should always anticipate that a field might be missing (undefined) or explicitly null. Use optional chaining and nullish coalescing to handle both safely.
// ── SERIALIZATION DEMO: undefined vs null ───────────────────────────────── const user = { name: 'Ananya', phone: null, // explicitly no phone number profilePic: undefined, // not set age: 28 }; // JSON.stringify will remove profilePic entirely, but keep phone as null const json = JSON.stringify(user); console.log('Serialized JSON:', json); // Output: {"name":"Ananya","phone":null,"age":28} // ── API RESPONSE DESIGN ─────────────────────────────────────────────────── // Backend should use null for missing values to preserve structure const goodResponse = { user: { name: 'Bob', email: null } // email field present but null }; const badResponse = { user: { name: 'Bob' } // email completely missing → undefined on client }; // Client-side handling const clientEmail = goodResponse.user.email ?? 'No email on file'; console.log('Good response email:', clientEmail); // 'No email on file' // This would crash if email was undefined and we accessed a property on it // But with ?? we are safe // ── PARSING: JSON.parse restores null exactly, undefined cannot exist in JSON ─ const parsed = JSON.parse('{"a": null}'); console.log('Parsed null:', parsed.a); // null console.log('Has own property a?', parsed.hasOwnProperty('a')); // true const parsedMissing = JSON.parse('{}'); console.log('Missing property:', parsedMissing.a); // undefined
Equality Checks — The Trap That Catches Every Beginner
Here's where things get genuinely dangerous if you're not careful. JavaScript has two equality operators: == (loose equality) and === (strict equality). With null and undefined, the difference between them is critical.
Loose equality (==) considers null and undefined to be equal to each other — and only to each other. So null == undefined is true. But null == 0 is false, and null == false is false. This is one of JavaScript's infamous quirks.
Strict equality (===) checks both value AND type. Since null and undefined are different types, null === undefined is false. This is almost always what you want.
In practice, there's one genuinely useful situation for the loose check: if you want to test whether a value is either null OR undefined — and you don't care which — you can write value == null. This is actually a common pattern in professional code, because API responses sometimes give you null and sometimes give you nothing at all (undefined).
For everything else, use ===. Predictability is more valuable than brevity.
// ── LOOSE EQUALITY (==) — Full of surprises ─────────────────────────────── console.log('--- Loose Equality (==) ---'); console.log(null == undefined); // true — they ARE loosely equal console.log(null == 0); // false — null does NOT loosely equal zero console.log(null == false); // false — null does NOT loosely equal false console.log(null == ''); // false — null does NOT loosely equal empty string console.log(undefined == false); // false — undefined does NOT loosely equal false // ── STRICT EQUALITY (===) — No surprises ───────────────────────────────── console.log('\n--- Strict Equality (===) ---'); console.log(null === undefined); // false — different types, different values console.log(null === null); // true — null is strictly equal to itself console.log(undefined === undefined); // true — same deal // ── THE ONE GOOD USE OF LOOSE NULL CHECK ────────────────────────────────── // This pattern checks: 'is this value either null OR undefined?' // Very useful when an API might return one or the other. console.log('\n--- Practical Null Check Pattern ---'); function processApiResponse(responseValue) { // This single check catches both null and undefined safely if (responseValue == null) { console.log('No value present (was null or undefined) — using default.'); return 'Default display text'; } return responseValue; } console.log(processApiResponse(null)); // null triggers the guard console.log(processApiResponse(undefined)); // undefined triggers the guard too console.log(processApiResponse('Hello!')); // real value passes through // ── typeof REVEALS THE DIFFERENCE ──────────────────────────────────────── // This is a famous JavaScript quirk worth knowing console.log('\n--- typeof Results ---'); console.log(typeof null); // 'object' <-- historical bug in JavaScript! console.log(typeof undefined); // 'undefined'
typeof null returns 'object' — not 'null'. This is a 25-year-old bug in JavaScript that was never fixed because fixing it would break the internet. To safely check for null, always use value === null, never typeof value === 'null' (that will never be true).typeof x === 'object' && x !== null, someone is working around the bug.undefined vs null: 8-Dimension Comparison Table
Here's a side-by-side comparison of undefined and null across eight key dimensions that every developer should know:
| Dimension | undefined | null |
|---|---|---|
| Declaration | Default for uninitialized variables | Must be explicitly assigned |
| Assignment | JavaScript assigns automatically | Programmer assigns deliberately |
| typeof | 'undefined' | 'object' (historical bug) |
| JSON.stringify | Omitted from output | Included as null |
| Loose equality (==) | undefined == null → true | null == undefined → true |
| Strict equality (===) | undefined === null → false | null === undefined → false |
| Purpose | Accidental 'no value' from system | Intentional 'no value' by developer |
| Data type | undefined (primitive) | null (primitive, but typeof lies) |
This table should be your quick reference whenever you're debugging null/undefined confusion. Memorize the typeof quirk — it's a common interview pitfall.
// Quick type checks console.log(typeof undefined); // 'undefined' console.log(typeof null); // 'object' — never use for null detection! // Correct null detection console.log(null === null); // true // Loose null/undefined guard console.log(null == undefined); // true (useful for combined check) // JSON behavior console.log(JSON.stringify({a: undefined, b: null})); // '{"b":null}'
Practical Patterns — Writing Code That Handles Both Gracefully
Knowing the theory is half the battle. The other half is writing code that handles null and undefined without crashing.
Modern JavaScript gives you several elegant tools for this. The optional chaining operator (?.) lets you safely drill into nested objects without throwing an error if something in the chain is null or undefined. The nullish coalescing operator (??) lets you provide a fallback value specifically when something is null or undefined — unlike the OR operator (||) which also replaces falsy values like 0 and empty strings, which can cause bugs.
Default function parameters handle the case where a caller doesn't pass an argument (undefined), but won't kick in for null — which is sometimes exactly what you want, and sometimes not, so it's worth being aware of the distinction.
These tools exist specifically because dealing with missing values is so common in real JavaScript. API calls fail, form fields are left empty, database records have optional columns. Professional code is full of these defensive patterns — not because developers are paranoid, but because data in the real world is messy.
// ── SCENARIO: Working with a user profile from an API ──────────────────── const loggedInUser = { name: 'Sofia Reyes', address: { city: 'Barcelona' // Note: no 'zipCode' property — it's missing entirely }, subscriptionLevel: null // User exists but has no active subscription (intentional null) }; const guestUser = null; // No user is logged in (intentional null) // ── OPTIONAL CHAINING (?.) — Safe property access ──────────────────────── // Without ?., accessing guestUser.name would throw a TypeError and crash your app. // With ?., if guestUser is null/undefined, it short-circuits and returns undefined safely. console.log('Guest city:', guestUser?.address?.city); // undefined (no crash!) console.log('User city:', loggedInUser?.address?.city); // 'Barcelona' console.log('User zip:', loggedInUser?.address?.zipCode); // undefined (property missing) // ── NULLISH COALESCING (??) — Safe fallback values ──────────────────────── // ?? only kicks in when the value is null OR undefined. // Unlike ||, it does NOT replace 0, false, or empty string. const displayCity = loggedInUser?.address?.city ?? 'City not provided'; console.log('Display city:', displayCity); // 'Barcelona' const displayZip = loggedInUser?.address?.zipCode ?? 'ZIP not provided'; console.log('Display ZIP:', displayZip); // 'ZIP not provided' const subscriptionLabel = loggedInUser.subscriptionLevel ?? 'Free tier'; console.log('Subscription:', subscriptionLabel); // 'Free tier' (because value was null) // ── WHY ?? IS SAFER THAN || FOR NUMBERS ─────────────────────────────────── const playerScore = 0; // A legitimate score of zero // BUG: || treats 0 as falsy and replaces it — wrong! const scoreWithOR = playerScore || 'No score yet'; console.log('Score with ||:', scoreWithOR); // 'No score yet' <-- WRONG! 0 is a real score // CORRECT: ?? only replaces null/undefined, leaves 0 alone const scoreWithNullish = playerScore ?? 'No score yet'; console.log('Score with ??:', scoreWithNullish); // 0 <-- Correct! // ── DEFAULT PARAMETERS — Handle undefined, NOT null ────────────────────── function createEmailGreeting(recipientName = 'Valued Customer') { // Default only fires when recipientName is undefined, not when it's null return `Dear ${recipientName}, thank you for your order!`; } console.log(createEmailGreeting('James')); // Default not used — real name provided console.log(createEmailGreeting()); // Default used — no argument passed (undefined) console.log(createEmailGreeting(null)); // Default NOT used — null is explicit!
createEmailGreeting(null), the default 'Valued Customer' does NOT kick in. You get the string 'null' rendered into your template. If null is a realistic input, add an explicit guard inside the function body: recipientName = recipientName ?? 'Valued Customer'.Nullish Coalescing (??) Cannot Mix with || or && Without Parentheses
When you start combining nullish coalescing with logical OR or logical AND, JavaScript enforces a strict rule: you cannot write something like a ?? b || c or a && b ?? c without parentheses. The language specification explicitly forbids mixing ?? with || or && unless you explicitly wrap one of the operators with parentheses.
This is because the precedence of ?? is deliberately vague next to || and &&. The JavaScript committee (TC39) decided that forcing developers to be explicit about intent would prevent subtle bugs. If you try to write a ?? b || c, you'll get a SyntaxError: "Cannot mix '??' and '||' without parentheses". The same applies for a && b ?? c.
To work around this, always add parentheses. For example: (a ?? b) || c or a ?? (b || c). The parentheses clarify which operation happens first and make your code easier to read and reason about.
This limitation often catches developers by surprise, especially those accustomed to freely chaining || and &&. It's a conscious design choice to improve code clarity.
// ── THIS THROWS A SYNTAX ERROR ─────────────────────────────────────────── // Uncomment the line below to see the error: // const result = null ?? 'fallback' || 'default'; // SyntaxError: Cannot mix '??' and '||' without parentheses // ── CORRECT USAGE WITH PARENTHESES ─────────────────────────────────────── // Option 1: Evaluate nullish coalescing first const option1 = (null ?? 'fallback') || 'default'; console.log('Option 1:', option1); // 'fallback' (?? returns 'fallback', then || sees truthy) // Option 2: Evaluate OR first const option2 = null ?? ('fallback' || 'default'); console.log('Option 2:', option2); // 'fallback' (|| returns 'fallback', then ?? sees truthy) // ── MIXING WITH && ─────────────────────────────────────────────────────── // Also throws error: // const invalid = value && value ?? 'default'; // SyntaxError // Correct: parenthesize const config = { timeout: 0 }; const timeout = (config.timeout ?? 3000) && config.timeout; console.log('Timeout:', timeout); // 0 (?? returns 0, && evaluates 0 to falsy, returns 0) // ── REAL-WORLD EXAMPLE: SAFE ACCESS WITH FALLBACK ──────────────────────── function getUserDisplayName(user) { // Without parentheses: error // return user?.name ?? 'Guest' || 'Unknown'; // SyntaxError // Correct: clarify intent return (user?.name ?? 'Guest') || 'Unknown'; } console.log(getUserDisplayName({ name: 'Alice' })); // 'Alice' console.log(getUserDisplayName(null)); // 'Guest' (?? gives 'Guest', then || uses it) console.log(getUserDisplayName({ name: '' })); // 'Unknown' (?? gives '', which is falsy, so || falls back to 'Unknown')
no-mixed-operators rule will catch this during development.Falsy Values: When ?? vs || Matters
A common source of bugs is assuming || (logical OR) and ?? (nullish coalescing) behave the same way. They don't. The || operator returns the right-hand side if the left-hand side is any falsy value: false, 0, '', null, undefined, NaN. In contrast, ?? only triggers on null or undefined.
This difference is critical when working with numbers that can legitimately be 0, strings that can be empty, booleans that can be false, or NaN. Using || in these cases will incorrectly replace valid falsy values with defaults, causing silent data corruption.
Here's a comparison table of how each operator behaves with these falsy values:
| Value | `value | 'default'` | value ?? 'default' | |
|---|---|---|---|---|
null | 'default' | 'default' | ||
undefined | 'default' | 'default' | ||
0 | 'default' | 0 (preserved) | ||
'' | 'default' | '' (preserved) | ||
false | 'default' | false (preserved) | ||
NaN | 'default' | NaN (preserved) |
Use || when you want to treat all falsy values as absent (e.g., a count that might be 0 should never be 0 — but that's rare). Use ?? when only semantic emptiness matters. In practice, most real-world data (scores, counts, empty strings) are legitimate values, so ?? is the safer default choice.
// ── OPERATOR COMPARISON ───────────────────────────────────────────────── const testValues = [null, undefined, 0, '', false, NaN]; console.log('Value → || result | ?? result'); console.log('─'.repeat(40)); testValues.forEach(value => { const orResult = value || 'default'; const nullishResult = value ?? 'default'; console.log(`${String(value).padEnd(10)} ${orResult.padEnd(10)} ${nullishResult}`); }); // ── PRACTICAL EXAMPLE: USER SETTINGS ──────────────────────────────────── const userSettings = { volume: 0, // User explicitly set volume to 0 (muted) nickname: '', // User doesn't want a nickname notifications: false // User disabled notifications }; // BAD: Using || replaces valid falsy values const displayVolume = userSettings.volume || 50; // 50! But user set volume to 0 const displayNickname = userSettings.nickname || 'Player'; // 'Player'! But user left it empty const displayNotifications = userSettings.notifications || true; // true! But user disabled console.log(`\nVolume (||): ${displayVolume} (expected 0)`); console.log(`Nickname (||): ${displayNickname} (expected '')`); console.log(`Notifications (||): ${displayNotifications} (expected false)`); // CORRECT: Using ?? preserves falsy values const correctVolume = userSettings.volume ?? 50; // 0 const correctNickname = userSettings.nickname ?? 'Player'; // '' const correctNotifications = userSettings.notifications ?? true; // false console.log(`\nVolume (??): ${correctVolume} (correct 0)`); console.log(`Nickname (??): ${correctNickname} (correct '')`); console.log(`Notifications (??): ${correctNotifications} (correct false)`);
prefer-nullish-coalescing) to enforce ?? over || in fallback expressions.Real-World Production Scenarios: Debugging and Preventing null/undefined Errors
In production, null/undefined bugs often surface in three repeatable scenarios: API response shape changes, missing data for edge cases, and incorrect default value handling.
Scenario 1: A third-party API adds a new optional field but returns null instead of omitting it. Your code uses || for fallback and accidentally replaces legitimate empty strings.
Scenario 2: A user completes an action that calls a callback, but the callback was never passed in — it's undefined. Calling it throws a TypeError that isn't caught.
Scenario 3: A migration script fails to set a default value for existing records, leaving a field as undefined. The frontend expects that field to always exist and crashes.
The common thread: assuming a value will always be present. Defensive coding is not paranoia — it's the difference between a 99.9% uptime and a P1 incident at 3 AM.
// ── SCENARIO 1: API response with unexpected null ───────────────────────── const apiResponse = { user: { name: 'Alice' }, settings: null // backend sent null instead of omitting }; // SAFE: Optional chaining + nullish coalescing const theme = apiResponse.settings?.theme ?? 'light'; console.log('Theme:', theme); // 'light' — no crash // ── SCENARIO 2: Callback might be undefined ─────────────────────────────── function performAction(onComplete) { // Do something... // SAFE: Only call if it's a function if (typeof onComplete === 'function') { onComplete(); } } performAction(); // No crash performAction(() => console.log('Done!')); // Works // ── SCENARIO 3: Frontend expecting a missing field ───────────────────────── const databaseRecord = { id: 1, name: 'Bob' }; // no 'email' field // SAFE: Provide default const email = databaseRecord.email ?? 'No email on file'; console.log('Email:', email); // 'No email on file' // ── LOGGING PATTERN FOR DEBUGGING ───────────────────────────────────────── function safeDeepAccess(obj, path) { return path.split('.').reduce((acc, key) => { if (acc == null) return undefined; return acc[key]; }, obj); } console.log('Safe deep access:', safeDeepAccess(apiResponse, 'user.profile.name')); // undefined
- A null value in a chain poisons all subsequent property accesses.
- Optional chaining stops the poison but silently returns undefined — now you have undefined instead of null.
- Always handle the result of optional chaining with a default or a check.
- Think of null as a deliberate 'stop' sign. Undefined is an accidental 'stop'.
null values for every optional field.5 Practice Exercises to Master null/undefined Handling
These exercises simulate real-world scenarios you'll encounter on the job. Try to solve each one before looking at the solution. Focus on safe property access, API response handling, and default parameter patterns.
// ── EXERCISE 1: Safe Property Access ───────────────────────────────────── // Given an API response that may be null or have missing nested fields, // safely display the user's city. If any part is missing, show 'Unknown'. const apiResponse = { user: { address: null } }; // or could be { user: null } // Your code: const city = apiResponse?.user?.address?.city ?? 'Unknown'; console.log('Exercise 1:', city); // 'Unknown' // ── EXERCISE 2: API Response Fallback ───────────────────────────────────── // An API returns a user object with a `role` field. If role is null or missing, // default to 'viewer'. If role is 0 (though unlikely), keep 0. const response1 = { role: null }; const response2 = { role: 'admin' }; const response3 = {}; // missing role becomes undefined function getRole(response) { return response?.role ?? 'viewer'; } console.log('Exercise 2a:', getRole(response1)); // 'viewer' console.log('Exercise 2b:', getRole(response2)); // 'admin' console.log('Exercise 2c:', getRole(response3)); // 'viewer' // ── EXERCISE 3: Default Parameter with null Guard ───────────────────────── // Write a function that prepends 'Dr. ' to a name, but falls back to 'Dr. Smith' // if name is undefined OR null. function greetDoctor(name) { // This doesn't handle null: // const safeName = name ?? 'Dr. Smith'; // But default parameter only catches undefined, so we need explicit check: const safeName = name ?? 'Dr. Smith'; return `Hello, Dr. ${safeName}`; } console.log(greetDoctor('Jones')); // 'Hello, Dr. Jones' console.log(greetDoctor()); // 'Hello, Dr. Smith' console.log(greetDoctor(null)); // 'Hello, Dr. Smith' (fixed!) // ── EXERCISE 4: Safely Calling a Method ─────────────────────────────────── // A config object may have an `onSuccess` callback. Call it only if it's a function. const config1 = { onSuccess: () => 'Success!' }; const config2 = {}; // no onSuccess function execute(config) { if (typeof config?.onSuccess === 'function') { return config.onSuccess(); } return 'No callback'; } console.log('Exercise 4a:', execute(config1)); // 'Success!' console.log('Exercise 4b:', execute(config2)); // 'No callback' // ── EXERCISE 5: Distinguishing null from undefined in Equality Checks ───── // Write a function that returns 'null', 'undefined', or 'value' based on input. function classifyValue(value) { if (value === null) { return 'null'; } if (value === undefined) { return 'undefined'; } return `value: ${value}`; } console.log('Exercise 5a:', classifyValue(null)); // 'null' console.log('Exercise 5b:', classifyValue(undefined)); // 'undefined' console.log('Exercise 5c:', classifyValue(0)); // 'value: 0' console.log('Exercise 5d:', classifyValue('')); // 'value: '
The typeof Operator Lies About null — Here's Why That Matters
You've probably seen it: typeof null === "object". This is a bug from JavaScript's first implementation that we're stuck with forever. It's not intentional. It's not clever. It's a historical accident. But you must know it because it breaks type guards. If you write if (typeof value === "object") thinking you're checking for a real object, null sneaks in. I've seen production apps crash because a null — assumed impossible — slipped past a type guard and called null.someMethod(). The fix: always check for null explicitly when using typeof "object". The typeof operator on undefined returns "undefined", which is correct. So you get one reliable falsy type check and one broken one. Remember which is which.
// What typeof actually returns console.log(typeof null); // "object" — THIS IS THE BUG console.log(typeof undefined); // "undefined" // Dangerous guard that fails function processData(data) { if (typeof data === "object") { // data could be null here! console.log(data.property); // TypeError if null } } // Safe guard function safeProcessData(data) { if (data !== null && typeof data === "object") { console.log(data.property); // safe } }
value !== null before accessing properties.Falsy Values Are Not Optional: Why Default Parameters Behave Differently
JavaScript default parameters only kick in for undefined, not for null or other falsy values. This catches everyone at least once. You write function greet(name = "User") expecting it to handle any falsy input. But when someone passes null, 0, or "", your default is ignored. The parameter gets the falsy value instead. This is by design: default parameters were created to handle the "missing argument" case — which produces undefined — not to sanitize bad input. If you want to handle all falsy values, you need manual checks or the nullish coalescing operator (??). The || operator defaults on all falsy values, which is often too aggressive. The ?? operator defaults only on null/undefined. Choose the tool that matches your intent: ?? for absence, || for falsy replacement.
function greet(name = "User") { console.log(`Hello, ${name}`); } greet(); // Hello, User (undefined triggers default) greet(null); // Hello, null (null does NOT trigger default) greet(0); // Hello, 0 greet(""); // Hello, (empty string) // Using ?? to handle both null and undefined function greetFixed(name) { const safeName = name ?? "User"; console.log(`Hello, ${safeName}`); } greetFixed(null); // Hello, User (?? catches null)
JSON Serialization Silently Deletes undefined — And That Breaks APIs
Here's a common production bug: you build an API response object, set some fields to undefined intentionally to omit them in the JSON, then wonder why your schema validation fails. The truth is more dangerous. JSON.stringify() silently removes all properties with undefined values. No error. No warning. Your object shape changes. Arrays with undefined elements become null instead. This is different from null, which serializes as "key": null — preserving the property and its intent. I've seen teams debug for hours because an optional field was missing from a response, breaking strict schema validation on the client. The fix: if you want a property to appear in JSON, use null. If you want it omitted, use undefined or delete. But be explicit. Default array initialization with Array(3) produces undefined slots — always fill() or map them to null before serialization.
const payload = { id: 42, name: "Alice", email: undefined, // will be removed phone: null, // will be preserved tags: [undefined, "active", undefined] // becomes [null, "active", null] }; console.log(JSON.stringify(payload)); // Output: {"id":42,"name":"Alice","phone":null,"tags":[null,"active",null]} // Safe pattern for API responses function buildResponse(user) { return { id: user.id, name: user.name, email: user.email ?? null, // explicit: use null for absence phone: user.phone ?? null }; }
?.) and get undefined, serialization silently drops that key. Always assume JSON.stringify swallows undefined — validate your output before sending to clients.The 'Cannot Read Property of Undefined' Production Outage
user: { profile: null } for a newly registered user who hadn't completed their profile. The frontend code used user.profile.name directly, causing a crash on null.user?.profile?.name ?? 'Guest'. Also enforced a rule: never access nested properties without checking the parent first.- Every nested property access is a potential crash point — treat null like a landmine.
- Use optional chaining and nullish coalescing as default patterns, not afterthoughts.
- Standardize API responses: missing data should be null, not undefined, so consumers can use a single guard pattern.
a.b.c). Log the parent object with console.log before the crash. Add optional chaining at the first likely null/undefined link.const { name } = user ?? {}. Or guard before destructuring.console.log(parentObj, 'parent just before crash');JSON.parse(JSON.stringify(parentObj)) to see the structure (or use structured clone).parentObj?.x and a fallback with ??.console.log(typeof value, value);If it's null, add guard: `value ?? 'fallback'`.console.log(typeof myFunction); // returns 'undefined'Check the import/require path and ensure the export exists.| Feature / Aspect | undefined | null |
|---|---|---|
| Who creates it? | JavaScript creates it automatically | You create it deliberately in your code |
| What it means | 'This slot exists but was never filled' | 'I intentionally set this to empty' |
| typeof result | 'undefined' | 'object' (historical JS bug — don't rely on this) |
| Strict equality (===) | undefined === undefined → true | null === null → true |
| Loose equality with each other | null == undefined → true | null == undefined → true |
| Default function params trigger? | Yes — undefined triggers defaults | No — null does NOT trigger defaults |
| Optional chaining (?.) triggers? | Yes — stops and returns undefined | Yes — stops and returns undefined |
| Nullish coalescing (??) triggers? | Yes — fallback value is used | Yes — fallback value is used |
| JSON.stringify behavior | Properties with undefined are omitted | Properties with null are included as null |
| When to use it in your own code | Almost never — let JavaScript handle it | When you want to explicitly signal 'no value' |
| File | Command / Code | Purpose |
|---|---|---|
| WhenJavaScriptCreatesUndefined.js | let userAge; // JavaScript quietly sets this to undefined | What 'undefined' Actually Means |
| WhenToUseNull.js | let currentUser = null; | What 'null' Actually Means |
| JSONSerializationBehavior.js | const user = { | JSON Serialization |
| EqualityCheckExplained.js | console.log('--- Loose Equality (==) ---'); | Equality Checks |
| ComparisonQuickRef.js | console.log(typeof undefined); // 'undefined' | undefined vs null |
| ModernNullSafePatterns.js | const loggedInUser = { | Practical Patterns |
| NullishCoalescingChaining.js | const option1 = (null ?? 'fallback') || 'default'; | Nullish Coalescing (??) Cannot Mix with || or && Without Par |
| FalsyOperatorComparison.js | const testValues = [null, undefined, 0, '', false, NaN]; | Falsy Values |
| ProductionDebugPatterns.js | const apiResponse = { | Real-World Production Scenarios |
| PracticeExercises.js | const apiResponse = { user: { address: null } }; // or could be { user: null } | 5 Practice Exercises to Master null/undefined Handling |
| typeof-trap.js | console.log(typeof null); // "object" — THIS IS THE BUG | The typeof Operator Lies About null |
| default-params.js | function greet(name = "User") { | Falsy Values Are Not Optional |
| json-serialization.js | const payload = { | JSON Serialization Silently Deletes undefined |
Key takeaways
typeof null returns 'object'=== null to check for null, never typeof.Common mistakes to avoid
4 patternsUsing separate === checks for null and undefined instead of a single == null guard
if (value === null || value === undefined). Temptation to skip one check leads to bugs when both values are possible.if (value == null) — this loose equality check catches both null and undefined safely and is a widely accepted pattern in professional JavaScript.Using || instead of ?? for null/undefined fallbacks when 0, empty string, or false are valid values
const displayName = userName ?? 'Guest'.Checking for null using `typeof value === 'null'`
typeof null returns 'object', not 'null'. The null value slips through unchecked, causing downstream crashes.value === null to check for null. Never use typeof for null checks. If you need to distinguish null from an object, use: value !== null && typeof value === 'object'.Assuming optional chaining (?.) will never return undefined — using the result without a default
user?.profile?.name ?? 'Unknown'. Or ensure the caller expects undefined.Interview Questions on This Topic
What is the difference between null and undefined in JavaScript, and can you give a real-world scenario where you'd choose to use null over just leaving a variable unassigned?
currentUser = null. That's a clear, intentional empty state. If you left it undefined, you couldn't distinguish between 'no one has logged in yet' and 'the variable has not been initialized'. Using null makes your intent explicit and easier to reason about.Why does `typeof null` return 'object' in JavaScript, and how would you correctly check if a value is null at runtime?
value === null. Never use typeof. If you need to distinguish null from an object, do: value !== null && typeof value === 'object'.What is the difference between the || (OR) operator and the ?? (nullish coalescing) operator for providing fallback values — and can you give an example where || produces a bug that ?? would prevent?
const count = apiResponse.itemCount || 0 works fine if itemCount is 0 because 0 is falsy and the fallback is also 0 — but const label = product.badge || 'No badge' will incorrectly replace an empty string '' with 'No badge' even though the API explicitly sent an empty string. Using ?? fixes this: product.badge ?? 'No badge' leaves '' intact because '' is not null/undefined.Explain how default function parameters interact with null and undefined. What happens when you pass null to a function with a default parameter?
function greet(name = 'Guest') { return 'Hello ' + name; } — calling greet(null) returns 'Hello null', not 'Hello Guest'. This often surprises developers. To handle null, add an explicit guard inside the function: name = name ?? 'Guest';.Frequently Asked Questions
With loose equality (==), yes — null == undefined evaluates to true. But with strict equality (===), no — null === undefined is false because they are different types. In professional code, use strict equality (===) unless you're intentionally writing a guard that catches both at once with value == null.
Almost never. JavaScript assigns undefined automatically for uninitialized variables, missing properties, and missing function arguments. If you want to express 'this has no value intentionally', use null instead. Assigning undefined yourself makes your code confusing because readers can't tell if it was deliberate or an oversight.
Every JavaScript function must return something. If you don't provide a return statement — or write a bare return with no value — JavaScript automatically returns undefined as the result. This is JavaScript filling in the gap. If your function is supposed to produce a value, double-check you're actually returning it, especially inside if-blocks or loops where it's easy to miss.
Yes! Optional chaining works with function calls: obj.method?.(). This safely calls the method only if it exists (is not null/undefined). If obj.method is null or undefined, the expression returns undefined instead of throwing a TypeError.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's JS Basics. Mark it forged?
8 min read · try the examples if you haven't