Learn by reading through in order

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.

Who Puts undefined and null There
Read adeclared-only varJavaScript fillsit inundefinedRead a missing keyor bad indexJavaScript fillsit inundefinedWant to signalemptinessThe writerassigns itnullFilled in automaticallyAssigned by the writer
In the top 2 rows, undefined is a value JavaScript fills in automatically; in the bottom row, null is a value the writer assigns.
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].

Where ?. Stops Reading
customerhas a value?.addresshas a value?.cityAustinshippingnull?.carrierstops hereundefinedpaymentno such key?.methodstops hereundefined
Where there's a value, it keeps going. The moment it hits null, undefined, or a missing key itself, it stops and returns undefined without reading the rest.
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.

Read values from an object shaped like an order API response, without stopping when fields are missing. response is already declared, and customer is always present.

① Display the shipping city, written so it doesn't stop even if something along the way is missing.

② Display the carrier name, shipping.carrier, with the same syntax.

③ Display the payment method, payment.method, with the same syntax.

④ Read a field name from a variable, and display the shipping zip code.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

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.

?? and || Diverge on Falsy Values
stock is 0|| checks itreplaces falsy20replacedstock is 0?? checks itreplaces only null0unchangedstock is null?? checks itreplaces only null20replaced
The results only diverge when stock is 0. ?? replaces only null and undefined, so 0 stays as the value.
Left-Side ValueWhat || ReturnsWhat ?? Returns
nullthe defaultthe default
undefinedthe defaultthe default
0the defaultstays 0
empty stringthe defaultstays an empty string
falsethe defaultstays 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".

Apply a default to each field of a user settings object, building the values you'll display. Fields that already have a value need to stay untouched. userSettings is already declared.

① Apply a default of "Guest" to the display name and display it.

② Apply a default of "en" to the display language and display it.

③ Apply a default of 20 to the items-per-page count and display it.

④ Apply a default of false to the unregistered notifications setting and display it.

JavaScript / TypeScript Editor

Run code to see output

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.

Compare how the display changes depending on which default operator you use, for a product with 0 stock and an empty note. product is already declared.

① Apply a default of 10 to the stock count with || and display it.

② Apply the same default of 10 to the stock count with ?? and display it.

③ Apply a default of "No notes" to the note with ||, wrap it in square brackets, and display it.

④ Apply the same default of "No notes" to the note with ??, wrap it in square brackets, and display it.

JavaScript / TypeScript Editor

Run code to see output

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.

?.() Skips the Call When Nothing's Registered
onSuccess isa functionCall with?.()It runsonError isnullCall with?.()Skipped,undefinedonError isnullCalledwithout ?.Stops witha TypeError
Call it if a function is there; return 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

Compare calling with ?. against calling without it, using an object that bundles order-processing handlers. handlers is already declared.

① Call the success handler using syntax that skips the call if it isn't registered.

② Call the failure handler the same way, and display whatever comes back.

③ Call that same failure handler without ?., and see what message stops execution.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1Write let couponCode; and read it without assigning a value. What's the variable's value?

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?