Compatibility
Published
Destructuring is the most common piece of modern syntax that nobody thinks of as syntax. It shows up in function signatures, in imports, in the first line of half the functions in a typical codebase, and it quietly carries default values for arguments that were not supplied. That makes it worth measuring carefully, because a transform that gets it slightly wrong does not produce a crash -- it produces a value you did not ask for, on a line that looks correct.
What was measured
The sample is twenty-four separate arms in one file, each printing a value a caller could assert on. Parameter object patterns with a whole-pattern default and with inner defaults. A parameter pattern with no whole-pattern default. A variable-position pattern. The strict distinction between an argument that is undefined and one that is null, which decides whether a default is taken at all. Nested patterns two levels deep. Key-to-different-local renaming inside a pattern. Array patterns with holes, defaults and a rest element. A swap. Object rest. Destructuring in a for-of head. A default that refers to an earlier binding in the same pattern. Catch-clause destructuring. And a pattern applied to an object parsed from JSON at runtime.
Each arm was protected in five configurations: the default output target, the modern output target, both gate profiles and the string-transform profile. In the base configuration -- no renaming, nothing enabled beyond the profile itself -- all twenty-four arms came back identical to the original on all five. Zero-valued and empty-string arguments still beat their defaults. null was still passed through rather than replaced. The dependent default still saw the earlier binding. Object rest still collected exactly the keys the named binding did not take.
That result is worth stating plainly before the caveats begin, because the caveats are all about an option you have to switch on: if you are not renaming members, destructuring is not a surface you need to think about.
A correction to something this site's own notes had wrong
A computed key in a destructuring pattern -- var { [k]: found } = source -- was previously recorded as a protect-time failure. Writing the key as anything other than a bare identifier, so { ['dyn']: v } or { [k + '']: v }, was documented as throwing an Invalid Expression during protection, on both targets, and it was tracked as a hard failure rather than a wrong value.
That is no longer the case, and this is a measurement rather than a recollection. A dedicated sample covering a bare identifier key, a string literal key, an expression key, a computed key in a parameter pattern, and a computed key in an ordinary object literal for contrast, protected and ran identically to the original on all five configurations. The engine's own emit-validation gate agrees, listing the computed-key sample as parsed, scanned and executed.
One variant of that gate sample does still fail, under self-compression on the modern target, and it is worth knowing why so you do not misattribute it. The gate's note on that variant says in as many words that it is independent of computed keys: the failure is a top-level var destructuring declaration combined with compression, which fails on a plain var { a } = o just as readily. That is a separate tracked issue covered elsewhere on this site, it fails loudly at build time rather than shipping bad code, and moving the declaration into a function or changing it to const clears it.
With member renaming, a pattern key is a property read
This is the part that surprises people, and it follows from one observation: var { total } = order is a read of the property total. It does not look like order.total, but it is the same access, and member renaming treats it as one. Whether that is safe depends entirely on whether the object being destructured had its keys renamed to match.
When both sides live in your own code, they move together and nothing happens. A pattern matching two field names the sample owns, applied to object literals written in the same file, measured identical to the original. Object rest behaved the same way: renaming the named binding left the collected remainder correct.
When the object comes from outside, they cannot move together, and the result is silent. The sample parses a small configuration blob from a JSON string and destructures it with defaults: var { retries = 1, delay = 10, backoff = 2 } = cfg. The real configuration says three retries and a fifty millisecond delay. With those three names renamed, the measured output was 1,10,2 -- every default taken, the parsed configuration silently ignored, nothing thrown. The keys in the JSON text are fixed characters in a string; the pattern that reads them is not.
One honest note on that arm. The same case on the modern target reported no difference, but the runner also reported that nothing in the file had been renamed at all, so that result says something about the pattern I chose and nothing about the engine. It is recorded here as inconclusive rather than as a pass, because a comparison where the transform did not run is not evidence.
The shape that desynchronises, and the one-line proof
There is a specific pattern shape where the two sides come apart even though both halves are in the same file. It is narrow, it is silent, and it substitutes a default rather than throwing.
The rule, read out of the emitted file rather than inferred from behaviour: wherever a pattern is emitted as a real destructuring pattern, a shorthand key with no default is correctly rewritten to the renamed key -- { code } becomes { _0x1: code } -- while a shorthand key that carries an inner default is left exactly as written. The object literal being destructured is renamed either way. So the pattern goes looking for a property name that no longer exists on the object, finds nothing, and takes its default.
The cleanest evidence is a single catch clause containing both halves at once. The source destructures a thrown object with catch ({ code, detail = 'none' }). The emitted file shows catch({_0x1:code,detail= 'none'}). The measured output went from E42/boom to E42/none: the key without a default kept working, and the key with a default silently fell back, in the same pattern, on the same line. A for-of head shows it twice over -- for (var { id, label = 'none' } of pairs) emitted as for(var {_0x1:id,label= 'none'} of pairs), and the identifiers stayed correct while every label became none.
The two targets differ here, and the reason is structural. The default output target lowers most patterns into explicit property reads before renaming happens, so the key is rewritten along with everything else and stays consistent -- a parameter pattern and a for-of head both measured clean there. Catch clauses are emitted as real patterns even on that target, which is why the catch arm fails on both. On the modern target patterns are kept as patterns, so the shape appears wherever a shorthand carries a default: parameters, variable position, nested patterns and loop heads alike.
Two notes on the status of this, because precision matters more than tidiness here. The engine separately tracks a known defect in the same area with a different trigger -- identifier renaming, on a parameter pattern carrying a whole-pattern default, where the pattern and the function body come apart. What is described above is the member-renaming shape, it needs no such trigger, and it reaches variable declarations, catch clauses and loop heads that the tracked one leaves alone. They are adjacent rather than identical.
And this is a defect rather than a designed trade-off. It was re-measured against a freshly built engine on the day this article was published, so treat it as a dated measurement rather than a permanent property of the tool, and re-run the check below on your own build instead of trusting this page.
Array patterns and rest elements are a different story
Everything above concerns object patterns, and that is not an accident: an object pattern reads properties by name, which is exactly what renaming acts on. An array pattern reads by position, and a position has no name to change.
The measurements bear that out. An array pattern combining a default, a skipped hole and a rest element produced the same result on every profile and under every member pattern tried, as did a two-variable swap through an array pattern. Neither is reachable by member renaming at all, because there is no property name anywhere in either construct. If your destructuring is positional, this whole article is about somebody else's code.
Object rest is the interesting middle case, because it names some properties and collects the others. Renaming the named binding left the collected remainder correct: the named property was still extracted and the rest still received exactly the keys it should have. The rest element does not enumerate a list of names, it takes whatever is left, so it follows the object rather than the pattern and stays consistent either way.
One caveat that belongs with rest rather than with renaming. The set of keys a rest element collects reflects the object as it exists at run time, so if the source object's own keys were renamed, the rest element collects the renamed keys. That is the documented behaviour for anything that reflects over property names, it is the same rule that governs a plain key listing, and it matters only if you then compare those keys against fixed strings.
What this means in practice
None of this is reached without member renaming switched on, and even with it on the exposure is bounded by two questions: does a pattern in your code read an object whose keys you do not control, and does that pattern carry defaults.
The first is the bigger risk and it has nothing to do with the shape above. Any pattern applied to parsed JSON, to a response body, to a configuration file or to data from another process is reading fixed key names, and renaming the pattern makes it miss them. Where the pattern has defaults, it will quietly install them; where it does not, you get undefined and a failure some distance away. Anchoring member patterns to names distinctive to your own application is the general mitigation this site recommends, and it is the right one here.
The second is narrow enough to check mechanically. If you rename members and you are on the modern target, a pattern of the form { name = fallback } is the shape to look for; rewriting it as { name: name = fallback } makes the key explicit and removes the ambiguity entirely, at the cost of looking slightly redundant.
The check itself is small. Take a function whose parameter pattern has a default, call it with an explicit value that differs from that default, and print what it received. If the printed value is the default rather than the argument, you have this shape. One call, one comparison, no test framework required.
Frequently asked questions
Does obfuscation break destructuring?
Not in the default configuration. Twenty-four arms covering parameter patterns, nested patterns, array patterns with holes and rest elements, object rest, catch-clause destructuring, for-of heads and dependent defaults were measured across five protection profiles, and all of them behaved identically to the original. Zero and empty-string arguments still beat their defaults and null was still passed through rather than replaced. Destructuring only becomes a surface worth checking when member renaming is switched on.
Are default values still applied correctly after obfuscation?
Yes, including the distinction that matters most. A default is taken for undefined and not for null, and that strict behaviour was preserved on every profile measured, as were zero, empty string and false arguments, which all correctly beat their defaults. A default that refers to an earlier binding in the same pattern also still saw that binding.
Why does my destructured variable have its default value instead of the real one?
Two causes, both tied to member renaming. If the object came from outside your code -- parsed JSON, a response body, a configuration file -- the key names in that data are fixed and the renamed pattern no longer matches them, so every default is taken. If the object is one of your own, look for the shorthand-with-default shape: a key written as { name = fallback } inside a pattern that is emitted natively is left un-renamed while the matching literal key is renamed, so the pattern misses and falls back. Writing it as { name: name = fallback } removes the ambiguity.
Do computed keys in a destructuring pattern still fail?
No. This was previously recorded as a protect-time parse failure on both targets and it has since been fixed. A sample covering a bare identifier key, a string literal key, an expression key and a computed key in a parameter pattern protected and ran identically to the original on all five profiles measured, and the engine's own validation gate lists the computed-key case as parsed, scanned and executed.
Is destructuring affected differently on the two output targets?
Yes, and it is the opposite of what people usually expect. The default target lowers most patterns into explicit property reads before renaming runs, so the keys are rewritten consistently and parameter patterns and for-of heads measured clean. The modern target keeps patterns as patterns, which is where the shorthand-with-default shape appears. Catch clauses are emitted as real patterns on both targets, so that one arm fails on both.
Does member renaming rename the keys inside a destructuring pattern?
Yes. A pattern key is a property read, so var { total } = order is treated the same way as order.total. That is safe when the object being destructured is one of your own and its keys are renamed to match, which is what the measurement showed for literals written in the same file. It is unsafe whenever the object's keys are fixed outside your build.
What is the quickest way to check my own build?
Call a function whose parameter pattern carries a default, pass an explicit value that differs from that default, and print what the function actually received. If it prints the default, you have the desynchronisation shape described above. For the external-data case, destructure a small parsed JSON object and print one field: if it reads undefined or falls back to a default, your pattern is renamed and the data is not.
Related reading