Q1Write let couponCode; and read it without assigning a value. What's the variable's value?
null and undefined — Reading Safely with ?. and ??
Sort out the difference between null and undefined, then read through missing nested values and set defaults safely.
Displaying order data from a server, you'll run into an address that was never filled in, or a payment method that hasn't been picked yet. JavaScript has two values for this "nothing's here" state — undefined and null — and a failed read stops processing with a TypeError.
This article covers the difference between the two, reading through a missing step without stopping, and applying a default value when something's missing.
The 2 Values Meaning Nothing's There — undefined and null
undefined (a value JavaScript automatically fills in when nothing's been assigned yet) shows up on its own — you don't need to assign it yourself. You've already seen it returned from reading past the end of an array or an unregistered object key, and a variable that's only been declared falls into this category too.
- A variable only declared, never given a value, like
let couponCode; - A key not registered on an object, like
order.customer(seen earlier) - An out-of-range index on a 2-element array, like
tags[5](seen earlier)
null (a value the writer assigns deliberately to signal emptiness), on the other hand, never shows up on its own. When you want the absence of a value itself to be data — "no shipping address has been chosen yet," "no coupon applied" — you assign null yourself.
JSON coming back from an API can also carry null to signal that a field is empty.
Since they're different values, === returns false when comparing them. ==, though, treats the two as the same and returns true.
When writing a check, either be deliberate about which one you're expecting and check just that side, as in === null, or hand it off entirely to ?. and ??, covered from here on.
let couponCode; // only declared, no value assigned
console.log(couponCode); // undefined
const order = { orderId: "ORD-1105", total: 12800 };
console.log(order.customer); // undefined (an unregistered key)
const tags = ["Limited", "Preorder"];
console.log(tags[5]); // undefined (an out-of-range index)
// null is assigned by the writer
let shippingAddress = null; // no shipping address chosen yet
console.log(shippingAddress); // null
console.log(shippingAddress === undefined); // false (they're different values)
console.log(shippingAddress == undefined); // true (== treats them as the same)
Not Stopping When a Nested Step Is Missing — Optional Chaining ?.
Data coming back from an API doesn't always have every field filled in. Code that reads deep, like order.customer.address.city, stops the moment even one order is missing an address partway through.
This is when you want the read itself not to fail — to keep going as "nothing there" instead.
When order.customer.address is undefined, trying to read .city past it throws TypeError: Cannot read properties of undefined (reading 'city'). The 1st level of reading goes through fine, but it stops at the 2nd.
That's what optional chaining is for (the ?. operator, which returns undefined without reading further if the left side is null or undefined). Write address?.city, and .city only gets read when address has a value — otherwise, the read stops there and returns undefined.
When passing a key through [], write it as ?.[field].
const order = {
orderId: "ORD-1105",
customer: { name: "Ryan Cole" },
};
// address is missing, so reading city past it would stop
// console.log(order.customer.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')
// Insert ?. and the read stops, returning undefined
console.log(order.customer.address?.city); // undefined
// Where there's a value, it keeps going as normal
console.log(order.customer.name); // Ryan Cole
// Pass a key through a variable with ?.[]
const field = "city";
console.log(order.customer.address?.[field]); // undefined
Use It Only Where a Field Might Legitimately Be Missing
Put ?. at every step, and even a typo'd key name slides through as undefined without erroring. Skip it on fields that are guaranteed to be there, and use it only right before a field that might legitimately be missing — that way the errors you actually want to catch still stop execution.
Setting Up a Default Value — ?? vs ||
Rather than showing a bare undefined when a field is missing, you'll usually want to display a default — "Guest," "20 items."
Use || for this, though, and sometimes a value that's actually there gets replaced by the default anyway. A stock count of 0, or an empty note field, are examples.
null and undefined together are called nullish. Use the Nullish coalescing operator (the ?? operator, which returns the right side only when the left side is nullish), and 0, an empty string, or false stay exactly as they are.
|| returns the right side whenever the left is falsy, so it can't tell "there's no value" apart from "the value is 0." When your goal is applying a default, ?? avoids the unintended replacement.
0. ?? replaces only null and undefined, so 0 stays as the value.| Left-Side Value | What || Returns | What ?? Returns |
|---|---|---|
| null | the default | the default |
| undefined | the default | the default |
| 0 | the default | stays 0 |
| empty string | the default | stays an empty string |
| false | the default | stays false |
const settings = { displayName: null, itemsPerPage: 0 };
// ?? returns the right side only when the left is null or undefined
console.log(settings.displayName ?? "Guest"); // Guest
console.log(settings.itemsPerPage ?? 20); // 0 (stays as-is)
// || returns the right side whenever the left is falsy, so even 0 gets replaced
console.log(settings.itemsPerPage || 20); // 20
// An unregistered key falls back to the default either way
console.log(settings.language ?? "en"); // en
console.log(settings.language || "en"); // en
Note that writing ?? and || side by side without parentheses, as in displayName ?? "Guest" || "Anonymous", is a syntax error — JavaScript can't decide which one to evaluate first. To use both, separate them with parentheses, as in (displayName ?? "Guest") || "Anonymous".
Don't Let a Default Wipe Out 0 or an Empty String
A stock count of 0, a discount rate of 0, an empty note — all of these count as "there's a value." Apply a default with ||, and since these are falsy, they get replaced by the default, creating a mismatch like an out-of-stock item displaying as in stock. Use ?? when all you care about is whether a value is there at all.
Not Calling Something That Isn't There — the ?.() Call
In order processing, you'll sometimes stash a "what to do on success" and "what to do on failure" inside an object, calling them when needed. Call a field that hasn't been set up with anything, though, and it stops with TypeError: handlers.onError is not a function — so you need a way to call it only when it's actually there.
The function covered in article 3 can also live inside an object as a property's value. The () => ... in the next example is syntax for creating a function on the spot — that syntax itself is covered in the syntax category, so here just focus on how the result changes depending on whether a function is in the property.
Call a property that might hold a function as handlers.onError?.(), and when onError is null or undefined, the call gets skipped and undefined comes back instead.
undefined without calling anything if it's null. Call it without ?. and it stops with a TypeError.const handlers = {
onSuccess: () => console.log("Order confirmed"),
onError: null,
};
// A function is there, so it gets called
handlers.onSuccess?.(); // Order confirmed
// It's null, so the call is skipped and undefined comes back
console.log(handlers.onError?.()); // undefined
// Calling without ?. stops execution
// handlers.onError();
// TypeError: handlers.onError is not a function
Knowledge Check
Answer each question one by one.
Q2When the left side of ?. is null or undefined, what happens to the rest of the chain?
Q3With const discountRate = 0;, what do discountRate || 5 and discountRate ?? 5 each return?