Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

??=, ||= and &&=: The Assignment Operators Nobody Told You About

C
Carlo Fontanos
· 2 min read

Every JavaScript codebase is littered with "assign if missing" statements. Since ES2021 they're single operators, and picking the right one prevents a classic bug in the process.

options.timeout ??= 5000;        // assign only if null/undefined
cache[key] ||= computeValue();   // assign if falsy
user.isAdmin &&= stillValid();   // reassign only if currently truthy

??= is the default-setter you want

The old idiom x = x || fallback has a hole: it clobbers every falsy value. Pass 0, empty string or false on purpose, and the fallback stomps it:

settings.retries = settings.retries || 3;   // retries: 0 becomes 3. Bug.
settings.retries ??= 3;                     // 0 survives; only null/undefined get 3

??= only assigns when the value is null or undefined - "not provided" rather than "provided but falsy". For config objects, function options and API defaults, that's almost always the semantics you meant.

They short-circuit the whole assignment

A subtlety that makes these more than sugar: if the condition fails, the right side never evaluates and no assignment happens at all:

// computeExpensive() doesn't run if cache[key] already set
cache[key] ??= computeExpensive();

// No setter call, no Proxy trap, no property write when skipped
observedObject.field ??= expensive();

With x = x ?? y, the write happens even when x keeps its value - which matters with setters, Proxies (like change trackers) and anything watching mutations. The ??= form genuinely skips the write.

Where ||= and &&= earn their spots

// ||= - when falsy really does mean "replace it"
draft.title ||= 'Untitled';         // empty string should become Untitled

// &&= - update a value only if it's currently set
session.user &&= refreshUser(session.user);   // stays null if logged out

The lazy-initialization pattern is my favorite everyday use, especially with the grouping trick:

(groups[type] ??= []).push(item);   // create-or-get the bucket, then push

One line that used to be three - though if you're grouping whole arrays, Object.groupBy does it natively now.

Support is everywhere post-2021 (Node 15+). Small feature, but the ||-vs-?? distinction it forces you to think about has probably fixed more bugs than the syntax saved keystrokes.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading