null, undefined, ?? and ?.

Beginner30 min

Tell null from undefined, supply defaults without clobbering 0 and empty text, and read nested data that may be missing.

What you will learn
  • Say where undefined comes from and when to use null
  • Use ?? instead of || for defaults, and explain the difference
  • Use ?. to read through values that might be missing
  • Use ??=, ||= and &&= for conditional assignment
On this page

Most programs deal with data that may not be there: a setting nobody set, a field a user left out, a lookup that found nothing. JavaScript has two values for “nothing”, and two modern operators that make handling them short. The general idea of a missing value is part of the fundamentals lesson on values and types. This lesson covers JavaScript’s version.

undefined and null

undefined means “no value has been given”. The language produces it for you in several situations.

undefined_sources.mjs
let declared;
console.log(declared);
const user = { name: "Ada" };
console.log(user.age);
console.log([10, 20][5]);
const empty = null;
console.log(empty, typeof empty, typeof undefined);
console.log(null == undefined, null === undefined);
Output
undefined
undefined
undefined
null object undefined
true false

Two things need a word of explanation. let declared; declares a variable without giving it a value, and the lesson on let and const covers let. { name: "Ada" } is an object, a record with named parts, and [10, 20] is an array, a list; the objects and arrays lessons come later.

In order: a variable declared without a value, a property that does not exist, and a position beyond the end of an array. All give undefined. Two more sources appear once you write functions: a parameter that was not passed, and a function that returns nothing.

null is a value you write yourself, to say “there is deliberately nothing here”. The language almost never produces null on its own. As the last lines show, typeof null is "object" (the old mistake from the first lesson of this module), null == undefined is true, and null === undefined is false.

A practical rule: let undefined be what the language gives you, and use null only when you want to state “empty” explicitly. Do not assign undefined yourself. Many style guides go further and avoid null altogether.

The nullish coalescing operator: ??

A common job is “use this value, or a default if it is missing”. The old way is ||, which uses the default for any falsy value, not only for missing ones. That removes legitimate 0, "" and false.

The operator ?? (ES2020) falls back only for null and undefined.

nullish.mjs
const count = 0;
const label = "";
const missing = null;
console.log(count || 10, count ?? 10);
console.log(label || "none", label ?? "none");
console.log(missing || "fallback", missing ?? "fallback");
console.log(undefined ?? null ?? "last");
console.log(false || null, false ?? "kept");
console.log((null ?? 1) || 2);
Output
10 0
none
fallback fallback
last
null false
1
  • count || 10 is 10 even though count is 0, a perfectly good value. count ?? 10 keeps the 0.
  • label || "none" replaces an empty string, and label ?? "none" keeps it. That is the empty line in the output.
  • For null, both operators use the fallback.
  • ?? chains: it returns the first value that is not null or undefined.

If you want “the default for anything falsy”, || is still right. The skill is choosing on purpose. For settings, quantities and flags, ?? is nearly always what you mean.

Optional chaining: ?.

Reading a property of undefined or null throws a TypeError. The optional chaining operator ?. (ES2020) stops and gives undefined instead, when the value before it is null or undefined.

optional_chaining.mjs
const user = { name: "Ada", profile: { address: null }, tags: ["math"] };
console.log(user.profile.address?.city);
console.log(user.settings?.theme);
console.log(user.tags?.[0], user.nothing?.[0]);
console.log(user.greet?.());
console.log(user?.profile?.address?.city.toUpperCase().length);
Output
undefined
undefined
math undefined
undefined
undefined

Without the ?., the same read stops the program. console.log(user.settings.theme) fails with:

TypeError: Cannot read properties of undefined (reading 'theme')

Three forms exist: a?.b for a property, a?.[0] for an index or computed key, and a?.() to call a function only if it exists. In the example, user.profile.address?.city is undefined because address is null. The error above names the property that was read from undefined, which is the first thing to look at when you see it.

The last line of the program shows short-circuiting: when address is missing, the whole rest of the chain, including .toUpperCase() and .length, is skipped and the answer is undefined. You do not need a ?. after every dot.

Use ?. where data may be absent, not everywhere. If a value must be there, a TypeError that points to the problem is better than an undefined that hides it.

?. and ?? are a natural pair: read something that may be missing, then supply a default.

Conditional assignment: ??=, ||= and &&=

Three assignment operators combine a test with an assignment (ES2021). The assignment happens only when the test says so.

Operator Assigns when the left side is
a ??= b null or undefined
a ||= b falsy
a &&= b truthy
logical_assignment.mjs
const options = { retries: 0, name: "", debug: undefined, verbose: true };
options.retries ??= 3;
options.name ||= "anonymous";
options.debug ??= false;
options.verbose &&= "yes";
console.log(options);
Output
{ retries: 0, name: 'anonymous', debug: false, verbose: 'yes' }

retries was 0 and stayed 0 with ??=. name was empty and was replaced with ||=. debug was undefined and got a default. verbose was truthy, so &&= replaced it.

Common pitfalls

  • Using || for defaults. It overwrites 0, "" and false. Use ??.
  • Mixing ?? with || or && without brackets. It is a syntax error.
  • Chaining ?. everywhere. It hides real mistakes, such as a misspelt property name.
  • Checking for missing values with typeof x === "undefined". Compare with undefined directly, or use x == null.
  • Assigning undefined to clear a value. Use null, or delete the property.
ExerciseRead nested config with defaults

A settings object config has an optional server object with host and port, and an optional debug flag. Build a new object settings with host (default "localhost"), port (default 8080) and debug (default false). A port of 0 and debug: false are valid and must be kept.

Start with const config = { server: { port: 0 }, debug: false };, then try {} and { server: { host: "example.test" }, debug: true } too.

Input: none. Expected output for the first config: { host: 'localhost', port: 0, debug: false }. For {}: { host: 'localhost', port: 8080, debug: false }. For the third: { host: 'example.test', port: 8080, debug: true }.

Show solution
config_defaults.mjs
const config = { server: { port: 0 }, debug: false };
const settings = {
host: config.server?.host ?? "localhost",
port: config.server?.port ?? 8080,
debug: config.debug ?? false,
};
console.log(settings);
Output
{ host: 'localhost', port: 0, debug: false }

config.server?.host is undefined when server or host is missing, and ?? then supplies the default. Because ?? ignores 0 and false, the port: 0 is kept. Using || would have turned that port into 8080.

ExerciseWhich operator?

For each case choose || or ??, then write the line and print its result: (a) a page size of 0, where 0 means “show nothing”, with the fallback 10; (b) a display name of "", which should become "Guest"; (c) a volume of 0, where 0 means muted, with the fallback 50.

Input: none. Expected output: 0, Guest, 0.

Show solution
which_operator.mjs
const pageSize = 0;
const displayName = "";
const volume = 0;
console.log(pageSize ?? 10);
console.log(displayName || "Guest");
console.log(volume ?? 50);
Output
0
Guest
0

(a) ??, since 0 is a real choice. (b) ||, since the empty string should be replaced, and ?? would keep it. (c) ??, since a muted volume must stay 0.

ExerciseFill in missing settings

Start with const prefs = { theme: "", fontSize: 0, language: undefined };. Use ||= for theme (an empty theme should become "light") and ??= for fontSize (default 16) and language (default "en"). Print prefs.

Input: none. Expected output: { theme: 'light', fontSize: 0, language: 'en' }.

Show solution
fill_prefs.mjs
const prefs = { theme: "", fontSize: 0, language: undefined };
prefs.theme ||= "light";
prefs.fontSize ??= 16;
prefs.language ??= "en";
console.log(prefs);
Output
{ theme: 'light', fontSize: 0, language: 'en' }

||= replaces the empty text. ??= leaves the 0 alone, because 0 is not null or undefined, and fills in the missing language.

Summary

  • undefined is what the language gives for “not set”. null is what you write for “deliberately empty”.
  • a ?? b falls back only for null and undefined. a || b falls back for any falsy value, including 0 and "".
  • a?.b, a?.[i] and a?.() return undefined instead of throwing when a is missing, and skip the rest of the chain.
  • ??=, ||= and &&= assign conditionally.

Next: variables and control flow, starting with let, const and block scope.