What survives
Published
Newer operators are the natural place to look for transform bugs, because the ES5 target cannot emit them and has to rewrite each one into something older. That rewrite is where behaviour usually gets lost. Measured operator by operator, this set comes through clean.
Why these are worth measuring separately
There are two target versions, and they do very different amounts of work. On the modern target an operator like ??= can be emitted as it was written. On the default ES5 target it cannot, so the engine has to express the same semantics using older syntax.
That rewrite is not mechanical. a ??= b assigns only when a is null or undefined, which is not the same as a || b, and it must not evaluate the right side at all when the assignment is skipped. Getting a short-circuit slightly wrong produces code that works on most inputs and misbehaves on falsy ones, which is the hardest class of bug to notice.
So each operator was executed rather than inspected: original and protected build both run in node, outputs diffed.
The results
a ??= "filled" against a null assigned the value. b ||= 5 against 0 assigned, because zero is falsy. c &&= 9 against 1 assigned, because one is truthy. Those three cases are chosen to be the ones that distinguish nullish from falsy, and all three matched the original on both targets.
Exponentiation matched: 2 ** 10 gave 1024, and the compound form e **= 2 gave 9 from 3.
Numeric separators matched in both bases: 1_000_000 came through as one million and 0b1010_1010 as 170. Separators are purely lexical, so the interesting question was whether the lexer drops or mangles them, and it does neither.
Optional catch binding matched: try { ... } catch { ... } with no parameter still caught and still ran its handler.
What the ES5 path actually has to do
It is worth being clear that “identical output” does not mean “identical code”. On the ES5 target these operators are gone from the emitted file; they have been rewritten. The behaviour is what was preserved, which is the only thing that matters at runtime and the only thing a test can check.
This also explains why the two targets are worth thinking about as a compatibility lever rather than a cosmetic setting. If you are shipping to environments that genuinely predate these operators, the ES5 target is doing real work for you. If your floor is a modern browser, the modern target asks the engine to rewrite less, and less rewriting means fewer places for behaviour to shift.
One combination still fails at build time, and it is a compression setting
The operators above all concern syntax the engine has to read, lower or preserve, and all of them came through cleanly. There is one modern-syntax combination that does not, and it is worth naming on this page because a reader asking whether modern syntax is safe deserves to know about the case that stops the build rather than changing the output.
Measured by driving the engine directly on a current build: with the modern target and SelfCompression enabled, a top-level destructuring declaration written with var fails to protect. Both var { a } = o and var [ x ] = arr do it. The engine reports an invalid-output error and produces nothing, rather than emitting code that misbehaves later.
The boundary is precise, and every arm of it was measured rather than inferred. The same declaration written with let or const protects cleanly. The same declaration moved inside a function protects cleanly. The default target protects cleanly under compression. And object spread on its own -- { ...src } -- is fine in every configuration, including with member renaming, which is worth stating because that combination was a defect until recently and is no longer one.
The reason this is worth knowing rather than filing away is where the pattern occurs. SelfCompression is enabled in the hosted default profile, and a top-level var { a, b } = require("...") is the shape of a great many bundled files. So the combination is easy to meet without doing anything unusual.
Two workarounds both clear it and neither costs anything: change the declaration keyword to const, or move the destructuring inside a function. As with everything else on this page, this is a tracked defect with a regression sample rather than a permanent property of the tool, so measure a current build before planning around it. The good news in the failure mode is that it is loud and it is at build time -- nothing ships.
How to check your own operator surface
This article measured a specific set. Your codebase will contain a different one, and the honest position is that a clean result here is evidence about these operators rather than a general guarantee about every construct you use.
The check that generalises is the one that does not depend on knowing which operators matter: run your existing test suite against a protected build. Tests that assert values catch semantic drift in exactly the places where a rewrite could have gone wrong, and they do it without anyone having to enumerate the language first.
If a specific expression is worth isolating, the smallest useful test is a file that prints the result of that expression, protected and unprotected, with the two outputs compared. That is precisely the method used above, and it takes a few minutes.
Frequently asked questions
Do logical assignment operators survive obfuscation?
Yes. Measured by executing the protected output, ??=, ||= and &&= all produced the same results as the original on both the ES5 and the modern target, including the cases that distinguish nullish from merely falsy.
Does ??= still skip the assignment for a value of zero?
Yes. Zero is not nullish, so ??= leaves it alone while ||= replaces it. Both behaviours were preserved in the protected build, which is the specific distinction a careless rewrite would lose.
Are numeric separators preserved?
The values are. 1_000_000 and 0b1010_1010 evaluated to one million and 170 after protection. Separators are lexical, so they do not survive as characters in the emitted file, but the numbers they describe are unchanged.
Does optional catch binding still work?
Yes. A catch block written without a parameter still caught its exception and ran its handler after protection on both targets.
Does the ES5 target still emit these operators?
No, and it is not supposed to. It rewrites them into older syntax that behaves the same way. What was measured and preserved is the behaviour, not the characters.
Does a clean result here mean all modern syntax is safe?
No. It is evidence about the operators listed here, measured on this engine. The way to get the same confidence about your own code is to run your existing test suite against a protected build.
Why does my build fail when I turn compression on?
Check for a top-level var destructuring declaration. Measured on a current build, the modern target plus SelfCompression plus a top-level var { a } = o or var [ x ] = arr fails to protect, reporting an invalid-output error rather than emitting bad code. The same declaration written with let or const is fine, the same declaration inside a function is fine, and the default target is fine under compression. It matters because SelfCompression is on in the hosted default profile and var { a, b } = require(...) sits at the top level of a great many bundles. Moving the declaration into a function, or changing var to const, both clear it. This is a tracked defect with a regression sample, so re-check against a current build.
Related reading