Compatibility
Published
Code kept in a string is not code as far as a protector is concerned. It is data: copied through untouched, never renamed, never analysed. That single fact explains everything this page measures, including the one case that genuinely stops working.
What was measured
The sample exercises five shapes of dynamic code. A direct eval that reads a local variable from the enclosing function. An indirect eval called through a variable, which the language evaluates in global scope. A new Function with named parameters. A helper compiled from a string built at runtime, with a numeric constant interpolated into it, which is the shape people reach for when they want a fast generated function. And a global read by name from inside an eval string, next to the same read written as a bracket access.
On the ES5 target with default options, on the modern target, and under the string-table preset, every line was identical to the unprotected run. new Function reported an arity of 2 and the name anonymous, exactly as the specification requires, and the compiled helper produced the same value.
Under the two identifier-renaming presets, one line changed. The direct eval that reads a local threw a ReferenceError where it had returned a number.
Why direct eval is the one that breaks
Direct eval is the only construct in the language that lets a string reach into the local scope around it. That is its whole purpose, and it is also why renaming defeats it: the local binding is renamed because it is code, the string is not renamed because it is data, and afterwards the string names a binding that no longer exists.
The failure is loud, which is the good kind. A ReferenceError naming an identifier you recognise, thrown at the first call, is about as debuggable as this class of problem gets. Compare it with the member renaming failures elsewhere on this site, where a boolean silently flips to its default.
The same mechanism appeared in our member renaming column. With a pattern matching a global's name, eval("exposedCounter") threw while the bracket form global["exposedCounter"] quietly returned undefined: two spellings of the same lookup, failing two different ways, because in each case the string half stayed put while the code half moved.
Everything else held. Indirect eval works because it evaluates in global scope and never depended on local names. The Function constructor works because its body is compiled in global scope by definition. Neither has anything to desynchronise.
The part that matters more than compatibility
If your protected bundle contains a helper compiled from a string, the body of that helper is in the output in plain text. Our sample interpolates a rate constant into a template and compiles it with new Function; that number and the surrounding code are readable in the protected file, because a string literal is a value to be preserved rather than a program to be transformed.
This generalises to every technique that keeps logic in strings: template compilers that build a function body, rule engines that assemble a condition, expression evaluators, and anything that stores a formula in configuration and evaluates it later. All of them are exempt from renaming, from control-flow changes, and from every other transform, because none of them are visible as code at build time.
So the practical order is: move logic out of strings and into real functions before protecting, not after. That is a source change rather than an option, which is why it is rarely mentioned, and it is the difference between protecting a program and protecting a loader for a program that is stored in the clear.
What the protector itself emits
It is worth being clear about the other direction, since it comes up in policy reviews. Two options can put an eval call in the output: the self-compression option, and the code transposition option that reorders statements and reassembles them at runtime. The emitted call has the shape eval(decoder("...", n)[0]), where the decoder unpacks a packed string.
Every other option produces output with no eval and no Function constructor in it. If your Content Security Policy omits unsafe-eval, which most modern policies do, those two options are the two to leave off. The policy pages linked below cover that decision in detail, including how it interacts with Trusted Types.
One more detail worth knowing: eval and Function both appear on the anti-tampering option's default watch list, along with Function.prototype.toString. So enabling that option means the build is watching those globals for replacement, whether or not your own code uses them.
A short checklist
Grep the source for eval(, new Function( and Function(. For each hit, ask whether the string reads any local name. If it does, that call breaks under renaming and needs rewriting; if it only reads globals or its own arguments, it works unchanged.
Then ask the more valuable question about each hit: is there logic in that string that you would rather not publish? If so, the fix is not a protection setting. Move it into a real function, or move it off the client, and protect the result.
Frequently asked questions
Does obfuscation break eval?
It breaks direct eval that reads a local variable, and we measured exactly that: a ReferenceError where the unprotected build returned a number, under both identifier-renaming presets. The local binding was renamed and the string naming it was not. Indirect eval and the Function constructor evaluate in global scope and were unchanged in every configuration.
Does new Function still work after protection?
Yes. We measured a two-parameter Function constructor reporting arity 2 and the name anonymous, returning the same result before and after protection in all five configurations, including a helper whose body was built from a string at runtime.
Is code inside an eval string protected?
No, and this is the more important point on this page. A string literal is preserved as a value, so a function body kept in a string is in the output in plain text, exempt from renaming and every other transform. Move that logic into real functions before protecting, or move it off the client.
Which options put an eval call in the protected output?
Self-compression, and the code transposition option that reorders statements and reassembles them at runtime. The emitted call has the shape eval(decoder("...", n)[0]). Every other option produces output containing neither eval nor the Function constructor.
What should I do if my Content Security Policy omits unsafe-eval?
Leave the two options above off, and the remaining transforms produce output a strict policy accepts. The dedicated policy article covers the decision in more detail, including how it interacts with Trusted Types enforcement.
How do I find the eval calls that will break before I ship?
Grep for eval(, new Function( and Function(, then check each string for a local name. Strings that read only globals or their own arguments keep working; a string that names a local is the one that throws, and it throws on the first call rather than silently.
Related reading