Compatibility
Published
An options object is safe to rename exactly as long as both sides of it live in code the obfuscator can see. Retry configuration is the cleanest illustration of that rule anywhere in this series, because the same object measures perfectly safe in one file and quietly catastrophic in the next, and the only thing that changed is where the values came from.
What was measured
The first sample is a self-contained retry helper: an options object with retries, factor, minTimeout and maxTimeout, a delay function that computes an exponential schedule capped at the maximum, a retry loop that gives up after the configured count, and a small circuit breaker with named states that opens after a threshold of failures. A fake clock accumulates the delays so the whole thing is deterministic and instant.
It exercises the two outcomes that matter: an operation that fails twice and then succeeds, and one that never succeeds. It records the attempt count, the total elapsed delay, the error message that came back, the full delay schedule and the sequence of breaker states.
Across five configurations the output was identical: three attempts and 300 milliseconds of accumulated delay for the recovering case, five attempts and 1500 milliseconds for the failing one, a schedule of 100, 200, 400, 800, 1000 showing the cap taking effect on the last step, and breaker states of closed, closed, open, closed.
The member column was clean too, on all three arms, which is unusual for this series. Renaming the options keys, the result fields and the breaker's own state fields all measured identical on both targets. That is the expected outcome for a structure whose reader and writer are both in the file: renaming is a consistent substitution, and a substitution applied to both sides of a read is invisible.
The boundary, and it is one line of code
The second sample changes exactly one thing. Instead of writing the options object as a literal, it parses it from a JSON configuration string -- the shape every real application eventually reaches, whether the JSON comes from a config file, an environment variable, a remote flag service or a database row. It then merges the parsed options over a defaults object and finally serialises the result as a telemetry payload.
Unprotected, this behaves as you would expect: the parsed values win over the defaults, the schedule is 50, 100, 200, 400, and the telemetry payload carries the four configured keys. With a member pattern matching those four option names, three things go wrong at once and none of them throws.
The direct reads return nothing. opts.retries and its neighbours were rewritten to generated names, while JSON.parse produced an object with the real names, so every read is undefined. The delay computation, being arithmetic on undefined, produced a schedule of NaN, NaN, NaN, NaN.
The merge then produced 5/3/10/100 where the source produced 3/2/50/400. The defaults object is a literal in the file, so its keys were renamed; the parsed object's keys were not; the merge copied both sets and the code read the renamed ones. Your production retry configuration was silently discarded and the library defaults took over.
Why a NaN delay is worse than a crash
It is worth following the NaN through to what it does in a real system, because this is the one result on this page that can take down a service rather than merely misbehave.
setTimeout does not reject a NaN delay. It coerces it, and the coercion produces zero. A retry loop whose backoff schedule is NaN therefore does not back off at all: it retries immediately, as fast as the event loop will let it, for as many attempts as it is configured to make. The entire purpose of exponential backoff is to stop a struggling service from being hammered by its own clients, and this failure mode removes precisely that.
The count is wrong in the same direction. With opts.retries reading undefined in the direct case and the default of five winning in the merged case, a deployment configured for three careful attempts becomes five immediate ones, per client, against a dependency that is already failing.
The telemetry made it plainest. The serialised payload came out carrying both sets of keys at once -- the generated names from the defaults and the real names from the parsed JSON, in the same object -- which is a wire format nobody designed and no consumer will parse correctly. And the validation loop that checks all four required keys are present still reported nothing missing, because the keys really were present. Just twice, under two different names.
The rule this makes concrete
This series has arrived at the same generalisation from a dozen directions: renaming is safe when a name is reached only by code the obfuscator can see, and unsafe when the name is part of a contract with something outside the file. Retry configuration is the sharpest test of that rule because it sits on both sides of the line depending on a single detail.
Write the options as a literal and pass it to your own function, and the contract is entirely internal. Both the object and every read of it are rewritten together, and the behaviour is exactly preserved. Load the same options from JSON and the writer is now a string of text that no transform will ever touch, while the readers are property accesses that a member pattern will.
The same asymmetry applies on the way out. A configuration object that is serialised and sent somewhere -- telemetry, a log line, a remote config echo -- is being read by something outside the file, and its key names are part of that contract whether or not you think of them that way.
So the practical form of the rule for configuration is narrower than the general one: an options object is safe to rename if it is created, read and discarded inside the protected code. If it crosses a boundary in either direction, at either end of its life, its key names belong to the boundary.
How to check your own build
The cheapest check is to assert on the schedule rather than on the behaviour. Compute your delay sequence for the first few attempts and compare it against the expected numbers. A renamed key shows up immediately as NaN, which no amount of successful retrying will reveal, since a working retry with the wrong delays still eventually succeeds.
If your configuration is merged over defaults, assert on the merged result and not on the parsed input. The merge is where a renamed defaults literal quietly wins, and it is the only place the substitution is visible as a plausible-looking wrong number rather than as undefined.
For anything serialised, assert on the JSON text. JSON.stringify writes whatever keys the object actually has, which makes it the most direct probe available for a renamed structure: if the output contains generated names, or contains the same setting twice under two names, a member pattern reached a boundary object.
And the structural fix, where it applies, is to stop reading configuration by property access at all. Code that pulls values out with a string key it already treats as data is unaffected by member renaming, because a string is not a rename site. That is not worth restructuring an application for, but it explains why some configuration layers are immune to this by accident.
Frequently asked questions
Does obfuscation break retry and backoff logic?
Not when the configuration lives in your own code. A retry helper with an exponential schedule, a maximum cap and a circuit breaker produced identical output across five protection configurations, and renaming its options keys, its result fields and its breaker state fields all measured identical too. Both sides of every read were rewritten together.
Why did my retry configuration revert to the library defaults?
Because the configuration arrived from JSON while the defaults were a literal in the protected file. Member renaming rewrote the defaults object's keys and every read, but JSON.parse produced an object with the original names, so the merge copied both sets and the code read the renamed defaults. Measured: a configured 3/2/50/400 became 5/3/10/100, silently.
What happens to the delay schedule when a key is renamed?
It becomes NaN, and that is the dangerous part. setTimeout coerces a NaN delay to zero, so a loop that should back off exponentially retries as fast as the event loop allows, for its full attempt count, against a dependency that is already failing. The measured schedule went from 50,100,200,400 to NaN,NaN,NaN,NaN.
Is an options object safe to rename or not?
It depends on one thing: whether the object is created, read and discarded inside the protected code. If it is, renaming is invisible and safe. If it is parsed from JSON, or serialised to telemetry or a log, or read by a library, then its key names are part of a contract with something outside the file and must not be renamed.
Will my validation catch this?
Probably not. A required-keys check on the merged object reported nothing missing, because the keys genuinely were present -- twice, under two different names. Presence checks pass while values are wrong, which is the same pattern seen elsewhere in this series when a renamed structure is inspected with string keys.
What is the cheapest assertion to add?
Compare your computed delay schedule for the first few attempts against the expected numbers, and assert on the JSON text of any configuration you serialise. The schedule catches a renamed read as NaN, and the serialised text catches a renamed key directly, since JSON.stringify writes whatever keys the object actually has.
Related reading