Failure Modes

When protection breaks something, it usually does not throw

This site has published several hundred measured case studies asking one question of one real library at a time: does obfuscation break it? The answer is consistent enough to be worth stating up front. Protection alone is repeatedly clean. What changes behaviour is member renaming - and when it changes behaviour, it very rarely produces an exception. It produces one of six shapes, all of which look like a successful run. If something is visibly broken right now, you want the diagnosis guide instead: obfuscated JavaScript not working works through the causes of a build that fails loudly. This page is about the ones that do not.

The Short Version

Two different questions

“Does obfuscation break my code?” and “does renaming my property names break my code?” are not the same question, and only the second one is dangerous.

ObfuscationChanges the implementation. The interface your callers see is unchanged.
RenamingChanges the interface. Anything reading a name you moved sees a different object.
The riskNot a crash. A run that succeeds, reports success, and is wrong.
The Six Shapes

What “it broke” actually looks like

Across the series the same six outcomes recur, on unrelated libraries in unrelated domains. Five of them leave a run that succeeds. Reading a green build as evidence of correctness is the mistake all six exploit.

1. It reverts to a default

An option key a library cannot read is indistinguishable from one you never passed, so the library uses its own value. Measured on a URL validator, an HTML sanitizer and a login rate limiter: the checks did not fail, they reverted - and the defaults were weaker than the policy they replaced.

2. It throws

The loud shape, and the one people plan for. It is the minority of cases, and it is the good outcome: a build that stops is a build you fix. Most of this page exists because the other five shapes do not stop anything.

3. A downstream reader ignores it

A member name that is serialised into a header, a log field, a metric label or a query string is read by a parser that is required to skip what it does not recognise. Measured on a Content-Security-Policy object: the header is present, the response is 200, and no directive is enforced.

4. The guard is quietly downgraded

When a library can no longer find the escaper, comparator or serialiser you supplied, it substitutes its own weaker built-in. The feature flag still reads on, the operation count is still right, and the telemetry still says the guard ran - because it did, just not yours.

5. The write lands somewhere else

A column, key or document field name is data, not API. An unrecognised one is not rejected: the write succeeds, reports rows affected, and creates a new field beside the real one, which keeps its old value. Measured against a real database driver, every column read undefined and totals became NaN.

6. The number becomes uncomparable

An unreadable numeric field does not become zero. It becomes undefined, undefined entering arithmetic gives NaN, and every comparison against NaN is false. Measured on a fraud score: an order that scored 75 and should have been blocked fell past the last branch and was allowed.

Direction

Open or closed is predictable before you ship

Whether a lost name lets something through or refuses everything is not a property of the field, its type, or how important it sounds. Three things decide it, and all three are visible at design time.

Permission or prohibition

A flag your collaborator hands back is read by somebody else. Write it as a permission - valid, ok, allowed - and a missing answer is falsy, so the decision becomes no. Write it as a prohibition - denied, blocked - and a missing prohibition permits. The phrasing costs nothing at design time and decides the direction of every later failure.

Ceiling or floor

A ceiling test asks “is this too much?” and is used to refuse; against an unreadable number it is false, the refusal is skipped, and it fails open. A floor test asks “is this enough?” and sits inside a negation; the same unreadable number makes the negation true and it fails closed, loudly. Same value, opposite directions.

Which side of the boundary reads it

If your own code writes and reads a name, renaming moves both together and nothing breaks - which also means a test built that way proves nothing. If a dependency, a server or a stored record supplies the name, only your read moves. Name the reader of each field before predicting anything.

The Argument That Does Not Hold

“If this broke, we would know”

That reasoning is only valid when the thing that would tell you is not downstream of the thing that broke - and in three separate measured cases it was.

The guard behind the flag

A strict check that fails loudly is only loud if it is still reached. Lose the enabling flag as well and the strict check is never consulted, so the pair produces the silent outcome, not the loud one. The half that would have screamed is switched off by the half that fails quietly.

The alarm that shares a name

When the detector and the thing it detects are the same name in your own code, one edit removes both. A prototype pollution check kept printing false while the prototype was being polluted, because the payload key is text in a document and the detector is a name in code.

Two layers, one file

Defence in depth only survives if the second layer lives where the transformation cannot reach it. Two independent controls on the same object in the same file fall to the same pattern - and each one alone looks like a cosmetic finding a reviewer closes.

What To Do

Configure so that failures are loud

Practical Guidance

Six rules that cost nothing at design time

  • Opt in, do not opt out. Mark private members with a naming rule and rename only those. An exclusion list is a denylist, and a denylist fails open on everything nobody thought of. See Protect Members for the rule syntax and Variable Exclusion List for the identifier equivalent.
  • Assume rules written as text do not move. Renaming rewrites names in code, never the text a rule is written in. A denylist of forbidden keys, a field name inside a parsed document, an allowlist of strings: the guard keeps running, keeps reporting itself installed, and matches nothing.
  • Validate what leaves your process. Check outgoing field names against the columns or keys that actually exist. That check fails closed when a name moves, which is the whole point - the write is otherwise accepted and lands beside the real field.
  • Require numbers to be finite, then count them. Refuse a non-finite value rather than comparing it. But note the inversion: where a limit is the minimum of several, filtering non-finite values out removes a limit instead of refusing, so assert the count of limits as well as their values.
  • Test the halves, not just the union. A safe default on one option can mask a destructive default on another, because the safe one is consulted first. The combined pattern then reads clean while a narrower, more careful pattern is the dangerous one. Dangerous cells are not at the wide end of the matrix.
  • Run the operation. A protect-time success and a passing syntax check say nothing about runtime. Execute the protected build against the record shapes that actually occur in production - a licence parsed from a file behaves differently from one built in the test.
Rename what you own, and nothing else
{
  "options": {
    "RenameMembers": true,
    "MemberRegexp": "^__"
  }
}

// only __privateName is eligible;
// every contract name is untouched
const cart = {
  __basketTotal: 0,   // renamed
  accountId: "a-17",  // stays
  isAllowed: true     // stays
};
The Evidence

Where each shape was measured

Every article below tests one real library, records what the correct run produces, and then compares it against the protected build. The base finding is the same in all of them: protection alone changed nothing.

By Surface

The rest of the research, grouped

Four hubs collect the articles that concern code you are likely to be shipping.

Language features

Closures, this, prototypes, symbols, getters, iterators, tagged templates and the rest of the syntax surface - what a transform is allowed to touch and what it must not.

Browser and DOM APIs

Option bags handed to the platform: fetch, cookies, CSP, workers, IndexedDB, form data, WebAuthn. The browser reads these names, and it did not read your build configuration.

Build and tooling

Bundlers, config merging, module resolution, code splitting, ORM models, generated clients - where protection sits in the pipeline and what it must run after.

Security controls

CSRF, JWT, sanitizers, rate limiters, permissions, signatures. The category where a silent revert costs the most, and where the direction rules matter most.

Frequently Asked

The questions behind the confusion

Does obfuscation on its own break working JavaScript?

In the measured series behind this page it repeatedly did not. Article after article records the same base result: protection alone is clean on every profile tested, and behaviour only changes once member renaming is switched on. Renaming is the option that changes an interface rather than an implementation.

Why does renaming so rarely throw an error?

Because a name a library does not recognise is not an error to that library. An unknown option reverts to a built-in default, an unknown field is accepted as data and stored beside the real one, and an unknown key in a serialised header is skipped by a parser that is required to ignore what it does not understand. Every one of those is a successful run.

What decides whether a control fails open or fails closed?

Two things, and neither is how important the field looks. First, phrasing: a flag written as a permission - valid, ok, allowed - is falsy when it goes missing, so the answer becomes no. A flag written as a prohibition - denied, blocked - goes missing and the prohibition disappears, so the answer becomes yes. Second, comparison shape: a ceiling test used to refuse is false against an unreadable number and the refusal is skipped.

If something this serious broke, would we not notice?

Only if the thing that tells you is not downstream of the thing that broke. A strict check sitting behind an enabling flag is never consulted once the flag is lost, so losing both is identical to losing the quiet half. Telemetry has the same problem: it can accurately report that a strong verifier is configured while nothing ever calls it.

Is an exclusion list enough to make renaming safe?

An exclusion list is a denylist, and a denylist fails open: anything you did not think of is renamed. The safer shape is the opposite one - mark private members with a naming convention and rename only what matches. Opting in is recoverable; opting out means discovering each contract by breaking it.

How should renaming be tested before release?

Run the operation, not just the build. A protect-time success proves nothing about runtime. Test individual names as well as the whole pattern, because a safe default on one option can hide a destructive default on another, and check the value that actually crosses the boundary rather than a flag reporting that a step ran.

Next

Protect the implementation first

Every transform other than renaming is contained: it changes a file and the file still presents the same interface. Start there, then opt in to renaming for the members you own.