Documentation
/

Troubleshooting

/

VM Obfuscation with 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 table below is the short version; the rest of this page explains each row.

What the obfuscator does, at a glance

Pattern in your sourceWhat happens
eval('literal string') (body is a string literal)Runs correctly (unless vmForceCompileDynamicCode is on, see Workarounds). The function that contains this call - and every function defined inside it - 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)The function that contains this call is VM-bytecoded normally. Indirect eval runs in global scope and cannot see surrounding local variables, so renamed locals cannot break it. It can still fail if the evaluated code references a global that was renamed or removed.
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.

The compiler also detects eval?.(code) conservatively and handles it like direct eval. JavaScript defines this optional-call form as indirect eval; the compiler's detection does not change that language behavior.

Only some of these patterns produce non-fatal warnings, and only when the call sits inside a function: a dynamic eval, new Function or Function call reports both DynamicCodeRenameRisk and VMDynamicCodeSkipped (VMDynamicCodeSkipped only for a function the VM would otherwise compile: not excluded, and marked when in comment mode; for a nested function it names the top-level ancestor that was skipped), a static eval('...') reports only VMDynamicCodeSkipped, and indirect eval and a fully static new Function report nothing. A dynamic call at the top level of a file, outside any function, is never reported. See Detecting the problem before runtime below for the warning types and a CI snippet.

The skip cascades through IIFEs. If a top-level IIFE wraps your whole bundle and any function inside it uses a direct eval (static or dynamic) or a dynamic new Function / Function call, the entire IIFE is skipped from VM bytecoding. See Direct eval() Disables VM Obfuscation for the IIFE-unwrapping pattern that limits the blast radius.

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) and indirect eval work differently: the body always runs in global scope, with access only to global variables and never to the locals around the call. They cannot depend on renamed locals, but they can still fail if they reference globals that were renamed or removed - or 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).

A static body that uses only its own parameters, such as new Function('a', 'b', 'return a + b'), has no such dependency: the body is a plain string the renamer never inspects. The obfuscator leaves the call in place and the surrounding function is still eligible for VM bytecoding. Changing the eval syntax alone does not make arbitrary dynamic code safe - review the warnings and test the final bundle.

The error you see at runtime

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

Text

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 some of these patterns in CI before shipping. Two warning types are relevant. Both are reported only for calls inside a function, except the .toString() case, which is reported wherever the call sits when the serialized function is nested in another function or cannot be resolved (VM builds only):

  • DynamicCodeRenameRisk - a function builds code from a string at runtime: a direct eval or a new Function / Function call whose body is not static, or fn.toString() injected into a <script> or Worker. This is the warning that predicts a ReferenceError.
  • VMDynamicCodeSkipped - VM bytecoding was skipped for a function because it contains a direct eval or a dynamic new Function / Function call. In root mode (the default) every function defined inside it is skipped too; in comment mode a nested function you mark separately is still virtualized unless it contains the call itself. It also fires for a safe static eval('literal'), so it signals lost VM protection rather than a runtime crash. Includes the function name (if available) and the construct that triggered the skip.

The javascript-obfuscator npm package does not expose warnings on its result, so a CI gate reads them from the API response: result and chunk_end messages carry a warnings array. The example below uses readObfuscationResponse(), the stream reader from the API Reference, and fails the build only on DynamicCodeRenameRisk; add VMDynamicCodeSkipped to the filter if losing VM protection on a function should also block the release.

Code

If a warning type is expected in your build and you would rather silence it at the source than filter it in CI, the warnings option (v7.8.0+) controls which warnings are emitted: 'none' suppresses everything, and a per-type map like { VMDynamicCodeSkipped: false } mutes just one type while keeping the rest.

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 - and for that reason, it cannot reference renamed locals either. The function that contains the call stays VM-bytecoded. The evaluated code must still not depend on globals the obfuscator renames.

  • 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 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. Don't annotate the function that contains the dynamic-code call, and mark the rest. Preserve these comments until the obfuscation step. See Targeting Specific 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. It cannot repair scope: inside a force-compiled function, direct eval cannot read or write the function's local variables or parameters, or variables of an enclosing function that is also virtualized, even when the code is a string literal. Reads throw ReferenceError, and writes are silently lost. A static eval('...') in this situation reports no warning at all, so a CI gate will not catch it; DynamicCodeRenameRisk still fires only for a dynamic body. Use it only when the evaluated code references nothing but globals. In the dashboard, this is the Force Compile Dynamic Code switch under the VM section's Overrides group.