Docs
/
Recipes
/

eval and new Function

VM Obfuscation with eval and new Function

How VM obfuscation handles dynamic code construction (direct eval and the Function constructor), what gets bytecoded versus skipped, the warnings the obfuscator emits, and how to diagnose ReferenceError at runtime.

Why this matters

VM obfuscation compiles function bodies down to bytecode dispatched through an in-runtime interpreter. Identifiers in the surrounding scope are also renamed. Both transformations interact badly with code that is built from a string at runtime: eval(s), new Function(...s), and Function(...s). If the runtime-built code references an identifier the obfuscator has renamed, you get Uncaught ReferenceError: <renamed-name> is not defined the first time the generated function runs.

The obfuscator handles each pattern differently. The matrix below is the short version; the rest of this page explains each cell.

What the obfuscator does, at a glance

Pattern in your sourceWhat happens
eval('literal string') (body is a string literal)Runs correctly. The function that contains this call loses VM bytecoding (direct eval reads the surrounding local variables, which the VM does not preserve once a function is compiled to bytecode).
eval(dynamicExpression)May crash at runtime with ReferenceError. The function that contains this call — and every function defined inside it — also loses VM bytecoding.
(0, eval)(s) / window.eval(s) (indirect)Runs correctly. The function that contains this call is VM-bytecoded normally. Indirect eval cannot see surrounding local variables, so renaming cannot break it.
new Function('a', 'b', 'return a + b') (all arguments are string literals)Runs correctly. The function that contains this call is VM-bytecoded normally.
new Function(dynamicBody) / Function(dynamicBody)May crash at runtime with ReferenceError. The function that contains this call — and every function defined inside it — also loses VM bytecoding.

All of these patterns are also surfaced as non-fatal warnings on the obfuscation result — see Detecting the problem before runtime below for the warning shapes and a CI snippet.

Why static and dynamic are treated differently

eval(s) can read and write local variables from the function it's called in. When s is a string literal, the obfuscator can parse the body at obfuscation time and rename identifiers consistently with the surrounding code. When s is a dynamic expression, the parsing only happens at runtime — by which point identifiers have already been renamed, so the runtime-built source references old names that no longer exist.

new Function(s) works differently: the body always runs as if it were defined at the top of your file, with access only to global variables and never to the locals around the call. That on its own is safe — but if you build the body by concatenating a renamed identifier into it (e.g. via func.toString() of a function whose internals the obfuscator has rewritten), the runtime-compiled function will still hit the same kind of ReferenceError.

Static new Function('return 42') never carries this risk: the body is a plain string the renamer never inspects, and at runtime it only needs to see globals. The obfuscator leaves the call in place and the surrounding function is still eligible for VM bytecoding.

The error you see at runtime

The common symptom is a ReferenceError the first time the dynamically built function runs:

browser console

Error

TU here is a renamed identifier the obfuscator introduced inside the bundle's IIFE scope. The dynamic eval / Function-constructor call evaluates a body that references it, but the body runs in a scope where TU is undefined.

Detecting the problem before runtime

The obfuscator emits non-fatal warnings via the API so you can catch these patterns in CI before shipping. Two warning types are relevant:

  • DynamicCodeRenameRisk — a function contains a dynamic eval / new Function / Function call whose body is built at runtime.
  • VMDynamicCodeSkipped — VM bytecoding was skipped for a function because of one of the above patterns. Includes the function name (if available) and the kind of construct that triggered the skip.

ci-build.mjs

JavaScript

Workarounds

  • Switch to indirect eval ((0, eval)(s))

    Only useful for the eval case. Indirect eval runs in global scope, so it cannot see surrounding local variables — but for that reason, it cannot reference renamed identifiers either. The function that contains the call stays VM-bytecoded.

  • Make the body fully static

    For new Function, if you can express the body as a single string literal / template literal with no interpolations, the call carries no rename risk and the function around it is still bytecoded. new Function('a', 'b', 'return a + b') is fine; new Function('return ' + expr) is not.

  • Move the call into its own top-level function, and unwrap the IIFE

    Each top-level function is checked independently. Lifting the dynamic-code-construction call into its own top-level function means only that one function loses VM bytecoding, instead of the skip cascading through a top-level IIFE that wraps your whole bundle.

  • Switch to vmTargetFunctionsMode: 'comment'

    Opt-in mode: only functions marked with /* javascript-obfuscator:vm */ are bytecoded. Skip annotating the function that contains the dynamic-code call, and bytecode the rest. See Targeting Functions.

  • Override the skip with vmForceCompileDynamicCode: true (v6.14.0+)

    Last-resort escape hatch. When enabled, the obfuscator bytecodes the surrounding function anyway and suppresses the VMDynamicCodeSkipped warning. Use only when you can guarantee that the runtime-built body never references an identifier the obfuscator renames — otherwise you trade a clean obfuscation for a ReferenceError at runtime. DynamicCodeRenameRiskstill fires so CI can keep gating on it. In the dashboard, this is the "Force Compile Dynamic Code" switch under the VM section's Overrides group.

Related pages