Troubleshooting

Obfuscated JavaScript not working? The six causes, in order

The code ran fine. You protected it. Now the console says undefined is not a function and the build is due. The good news is that this failure is remarkably unimaginative — nearly every case is one of six things, and the first one accounts for most of them. Work down the list.

1. Something matches your name as a string

This is the cause. If you read nothing else, read this section.

A renamer can safely rename a reference the compiler can see. It cannot see a reference that is resolved from text at runtime. Every one of these is a string lookup wearing a disguise:

  • onclick="doThing()" in server-rendered HTML, or any inline event attribute.
  • A framework binding — a prop name, an emitted event name, a route path, a named route, a template-referenced method.
  • A key read off a JSON response: res.data.subscriptionTier is a contract with your backend, not an internal name.
  • A localStorage / sessionStorage / IndexedDB key written by an old release and read by the new one.
  • Computed member access: obj[fieldName] where fieldName arrives as a string.
  • A public SDK method that a customer’s page calls, an analytics tag, an embedded checkout script, a widget host.

The fix is reservation, not weaker protection. Put those names in the Variable Exclusion List (a multi-line list of regular expressions — ^myname$ matches exactly one identifier), or in reservedNames in your config, or use desktop project exclusions. The compatibility validation guide walks the same boundary for framework and integration contracts.

How to find the offender fast: read the actual runtime error instead of re-running the build. x.subscriptionTier is not a function and cannot read properties of undefined (reading 'plan') both name the property that failed to resolve. That name is your answer.

2. You protected a compiler input, not a compiler output

Obfuscation belongs after the build, never inside it. Raw .ts, .tsx, .jsx, Vue single-file components and framework templates are read as written by the toolchain that compiles them; transforming them first breaks the parse or corrupts the semantics the compiler was about to rely on.

The correct order is always the same: build, then protect the emitted JavaScript into a separate folder such as dist-protected, then deploy that folder. Keeping it separate also means rollback compares two artifacts cleanly instead of one folder against its own past self.

3. eval, new Function, or anything that reads its own source

Code that constructs code from text has assumptions about that text. So does anything calling Function.prototype.toString() on itself — a surprisingly common pattern in worker bootstrapping, template compilers, and dependency-injection frameworks that parse parameter names out of a function signature. Angular’s classic DI is the textbook example: parameter names are the injection tokens, so renaming them breaks resolution unless you use the array-annotation form.

If a library needs to read source text to work, that region needs to stay verbatim. Which leads to…

4. You need a carve-out, and you are using the wrong tool for it

Splitting a file in two to protect half of it is a maintenance tax nobody pays for long. Inline directives mark regions in the source instead:

  • // javascript-obfuscator:disable// javascript-obfuscator:enable — everything is protected except that region, which is copied verbatim. Enabled with --honor-conditional-comments.
  • // javascript-obfuscator:protect-begin// javascript-obfuscator:protect-end — only the marked region is protected. Enabled with --protect-marked-comments.

Use one style per file. Note the CLI deliberately fails by default when it finds disable markers it has not been told to honor — that is there so a stray marker cannot silently ship an unprotected region.

5. The files were protected in separate runs

Cross-file transforms depend on a single shared mapping. Protect vendor.js on Monday and app.js on Tuesday and any global or member renamed in one will no longer match the name the other still calls. Symptom: everything works until a lazily loaded chunk arrives, then a function that definitely exists is suddenly undefined.

Protect the whole set in one pass, including lazy route chunks. And skip what should not be in the pass at all — vendor bundles, polyfills, and framework runtime files are usually better excluded than protected.

6. It really is a bug — here is how to prove it

Sometimes the answer is not your configuration. Modern JavaScript is a large surface, and a protection engine that lowers or rewrites syntax can get an edge case wrong. To turn “it broke” into something actionable:

  1. Bisect the transforms. Drop to identifier renaming only. Works? Re-enable one transform at a time until it fails. The transform that flips it is the report.
  2. Bisect the input. Cut the failing file down until you have the smallest snippet that still misbehaves after protection. A ten-line repro gets fixed; a 2 MB bundle gets triaged.
  3. Compare behavior, not text. Run the original and the protected output and diff what they do. Output that merely looks strange is usually fine; output that computes a different answer is a defect.
  4. Check the syntax level. Confirm the protected file parses at all — node --check protected.js is a two-second check that separates “invalid output” from “valid output, wrong behavior.”

With a minimal repro, send it in. That is genuinely the fastest path, and a reproducible snippet is worth more than any description of the symptom.

Bonus: it works, but you cannot debug it

If the build is fine and the problem is that production stack traces are now full of generated names, that is not a failure — it is the expected result, and it has a workflow. Keep each build’s identifier map privately and translate traces back with symbolication. See debugging obfuscated JavaScript in production.

The habit that prevents all of this

Test the protected artifact, not the unprotected one. Treat dist-protected as its own release candidate and run the real path through it — sign in, hit a guarded route, submit a form, load a lazy chunk, refresh a page that rehydrates persisted state. Every failure above surfaces in that smoke test in under two minutes, on your machine, instead of in production on a Friday.

Related reading