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 source | What 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:
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 directevalor anew Function/Functioncall whose body is not static, orfn.toString()injected into a<script>or Worker. This is the warning that predicts aReferenceError.VMDynamicCodeSkipped- VM bytecoding was skipped for a function because it contains a directevalor a dynamicnew Function/Functioncall. 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 staticeval('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.
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
evalcase. 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
VMDynamicCodeSkippedwarning. It cannot repair scope: inside a force-compiled function, directevalcannot 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 throwReferenceError, and writes are silently lost. A staticeval('...')in this situation reports no warning at all, so a CI gate will not catch it;DynamicCodeRenameRiskstill 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.
Related pages
- Direct eval() Disables VM Obfuscation - the IIFE-unwrapping pattern for limiting the skip's blast radius.
- Targeting Specific Functions - using
vmTargetFunctionsModeto opt in or out at function granularity.
