JavaScript Intermediate #5 Optional Chaining and Nullish Coalescing

Following #4 Destructuring/spread in depth, this post covers two short but powerful ES2020 operators — optional chaining (?.) and nullish coalescing (??).

?. — optional chaining #

The operator for safely accessing deep objects.

old way vs optional chaining
// old
const street = user && user.address && user.address.street;

// modern
const street = user?.address?.street;

Half the length for the same job. If user is null/undefined, it stops there and returns undefined. It doesn’t continue.

How it works #

?. in one line:

If the left value is null or undefined, stop and return undefined. Otherwise, evaluate normally.

rule check
const obj = null;

obj?.foo;           // undefined (stops because of null)
obj?.foo.bar;       // undefined (stopped — .bar isn't evaluated)

const obj2 = { foo: { bar: 42 } };
obj2?.foo?.bar;     // 42 (all meaningful values)
obj2?.unknown?.x;   // undefined

If any link is null/undefined, stop there — result is undefined.

Function call — ?.() #

Safely call a method/callback that may not exist.

call only when present
const callback = options.onSuccess;
callback?.();

obj.greet?.();

If callback is defined, call it; otherwise nothing happens. Common in functions that take callback options.

Array/brackets — ?.[] #

safe array access
const items = config?.items;
const first = config?.items?.[0];
const dynamic = obj?.[someKey];

You can’t append brackets directly to ?. — write ?.[] or ?.[key].

Pitfall of ?. — don’t use everywhere #

Convenient as it is, peppering optional chaining everywhere hides bugs.

overuse pitfall
function processUser(user) {
  return user?.id?.toUpperCase();   // ✗ overly defensive
}

If user is genuinely optional, fine. But if user should always be passed, ?. silently swallows the bug. A null user could be the real problem.

A good guideline: use ?. only where things really might be missing. Don’t sprinkle it “just in case.”

?? — nullish coalescing #

The operator that gives a default value when something is null or undefined.

?? basics
const name = user.name ?? 'anonymous';
const age = user.age ?? 0;

If the left value is null/undefined, take the right; otherwise keep the left.

?? vs || — falsy vs nullish #

In the past, || did the same job. The difference is subtle but important.

comparing || and ??
const a = 0 || 10;       // 10  (0 is falsy)
const b = 0 ?? 10;       // 0   (0 isn't nullish)

const c = '' || 'default';   // 'default'
const d = '' ?? 'default';   // ''

const e = false || true;     // true
const f = false ?? true;     // false

|| triggers on all 7 falsy values (false/0/-0/0n/’’/null/undefined/NaN). ?? triggers on exactly two — null/undefined.

This matters because 0, '', false, and other meaningful values are falsy, which caused frequent bugs.

old || pitfall
function withTimeout(timeout) {
  const ms = timeout || 5000;   // ✗ blocks timeout=0
  // ...
}

withTimeout(0);   // ms = 5000, 0 is ignored
?? is correct
function withTimeout(timeout) {
  const ms = timeout ?? 5000;
  // ...
}

withTimeout(0);   // ms = 0

For default-value patterns, almost always ?? matches your intent. Use || only when “if empty or zero, use default” really is the intent.

When the two meet — ?. + ?? #

Often paired.

optional + default
const username = response?.user?.name ?? 'anonymous';
//                        |               |
//                        |               default for the empty slot
//                        deep-safe access

const port = config?.server?.port ?? 3000;

Safely access a deep place and default if missing — solved in one line. One of modern JavaScript’s most common idioms.

??= and friends — logical assignment operators #

A short form added in ES2021.

??= — assign only when null/undefined
let user = { name: 'Curtis' };

user.email ??= 'default@example.com';
// assigns if user.email is null/undefined; otherwise leaves it

There are three variants.

three logical assignments
a ||= b;    // if a is falsy, a = b
a ??= b;    // if a is nullish, a = b
a &&= b;    // if a is truthy, a = b

||= vs ??= is the same difference as before — falsy vs nullish.

Pitfall — operator precedence #

When mixing ?? with ||/&&, parentheses are required. JavaScript catches it at compile time.

mixing requires parens
const a = null ?? true && false;     // ✗ SyntaxError
const b = (null ?? true) && false;   // OK — false
const c = null ?? (true && false);   // OK — false

The language blocks ambiguous expressions. Reduces mistakes.

Optional chaining and destructuring #

optional + destructuring
const { user: { name } = {} } = response ?? {};

Looks complex but two steps:

  1. response ?? {} — start with an empty object if response is null/undefined
  2. user: { name } = {} — start with an empty object when user is missing (so name = undefined)

If this gets too tight, splitting it out is better.

split — often clearer
const user = response?.user;
const name = user?.name ?? 'anonymous';

Optional chaining vs simple checks — which is better? #

simple check vs chaining
// optional chaining
if (user?.address?.street) {
  // ...
}

// explicit check
if (user && user.address) {
  // ...
}

In most cases ?. is shorter and more intuitive. But when the check itself is the intent, an explicit check expresses intent better. Largely a convention difference — follow team style.

Wrap-up #

What we covered:

  • ?. — stops and returns undefined when the left is null/undefined
  • Use ?.(), ?.[] for calls / array access
  • Don’t sprinkle ?. everywhere — only where things are truly optional
  • ?? — triggers only on null/undefined
  • ?? vs || — preserves meaningful values like 0, ‘’, false
  • Default-value patterns almost always want ??
  • ??=, ||=, &&= — logical assignment operators
  • Parentheses required when mixing ?? with ||/&&

In the next post (#6 fetch API and Error Handling) we cover the standard network tool that works in both browser and Node — fetch usage, error handling, and AbortController.

X