CoffeeScript's Existential Operator: The Null-Safety Idea JavaScript Eventually Adopted
CoffeeScript's ? and ?. operators were a deliberate null-safety design that JavaScript later adopted as ?. and ??. Here's what they compile to and how to migrate them without semantic drift.
13 Dec 2025, 11:40 UTC

If you inherit a CoffeeScript codebase, the first thing that will trip you up isn't the significant whitespace — it's the question marks. user?.profile?.name, fn?(), if config? — they look like typos until you realize they're the feature that made CoffeeScript worth using in the first place. The useful takeaway: CoffeeScript's ? and ?. operators are null-safety guards that compile to explicit null/undefined checks, and modern JavaScript's optional chaining (?.) and nullish coalescing (??) are essentially the same idea, standardized a decade later. That makes these operators both easy to read in legacy code and easy to migrate.
The problem the operator was solving
JavaScript has two "empty" values, null and undefined, and pre-ES2020 it gave you no terse way to handle them. A defensive property lookup looked like this:
var name = user && user.profile && user.profile.name;
if (name == null) { name = 'anonymous'; }Worse, the && chain conflates nullish values with other falsy ones. If user.profile.name is legitimately 0 or "", the guard still treats it as missing. CoffeeScript's designers made a deliberate engineering decision: give developers one terse idiom that means exactly "not null and not undefined" — nothing more, nothing less.
What ? and ?. actually compile to
The existential operator a? asks "does a exist?" It compiles to roughly:
typeof a !== "undefined" && a !== nullNote what this is not: a truthiness check. 0? and ""? are both true in CoffeeScript, because those values exist. Only null and undefined fail the test.
The soak operator a?.b ("soak up the null") guards the access itself. If a is null or undefined, the entire expression evaluates to undefined instead of throwing a TypeError. It chains (a?.b?.c) and works on function calls: callback?(args) calls the function only if it exists. The compiled output is a guarded ternary — you can confirm this yourself with the official compiler:
# Run in a shell with coffeescript installed (npm i -g coffeescript)
coffee -c -p -e "name = user?.profile?.name"The -p flag prints the compiled JavaScript to stdout instead of writing a file, so you can inspect the emitted guard code directly. No special permissions needed; the only risk is accidentally omitting -p and overwriting a .js file in the working directory.
A worked example, then and now
Here's the migration teaching case in full. CoffeeScript:
name = user?.profile?.name ? 'anonymous'The trailing ? 'anonymous' is the existential operator used as a binary "default" — if the left side is null or undefined, use the right side. The modern JavaScript equivalent is nearly character-for-character:
const name = user?.profile?.name ?? 'anonymous';The semantic match is close because both languages drew the same line: nullish-only, not truthy-based. You can verify parity by running the JS version in any current Node.js LTS and comparing behavior against the compiler output for a few edge inputs: undefined, null, 0, "", and a populated object.
The trade-off that bites during migration
The biggest risk isn't the syntax — it's semantic drift. CoffeeScript's binary a ? b is nullish-only, and so is JavaScript's ??. But developers migrating code by hand often reach for the more familiar ||, which is truthy-based. That single substitution changes behavior for 0, "", false, and NaN:
| Input | CoffeeScript a ? 'x' | JS a ?? 'x' | JS a || 'x' |
|---|---|---|---|
null | 'x' | 'x' | 'x' |
0 | 0 | 0 | 'x' |
"" | "" | "" | 'x' |
Two more caveats. First, CoffeeScript 1.x and CoffeeScript 2 emit different JavaScript (CS2 targets ES2015+ with classes and modules), so compiled output you find in an old blog post may not match what your toolchain produces — check which version the codebase pins. Second, CoffeeScript is in maintenance mode: the compiler still works and still compiles these operators correctly, but active feature development has ended. Check the project's repository for current release status before relying on that claim, since maintenance posture can change.
What to do with this
If you're maintaining CoffeeScript: read ? as "exists" and ?. as "guard this access," and remember both are nullish-only. If you're migrating: the mapping to ?. and ?? is almost mechanical — just never let a || sneak in as a substitute. A practical check: pick one file, compile it with coffee -c -p, hand-translate it to modern JS, and run both against the same edge-case inputs. If the outputs agree on 0 and "", your translation preserved the semantics that made the operator worth having in the first place.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.