Writing Self Modifying Code in JS Architecture
Self-modifying code changes its own behavior at runtime. Learn how JavaScript enables this through eval, the Function constructor, Proxy, and monkey-patching, and when to avoid it.
Self-modifying code is code that changes its own structure or behavior at runtime. Instead of executing a fixed set of instructions determined at authoring time, the program rewrites part of itself while it runs.
JavaScript supports this idea through several built-in mechanisms. Some are explicit and dangerous, like eval(). Others are structured and safe, like the Proxy API.
Understanding the full spectrum helps you recognize these patterns in real code and decide when they are appropriate.
The Mechanisms
JavaScript provides four main ways for code to modify its own behavior at runtime:
Each mechanism has a different risk profile and set of valid use cases. The diagram orders them from most dangerous (eval in local scope) to most structured (Proxy with defined traps).
eval() -- Code From Strings
eval() takes a string and executes it as JavaScript in the current scope:
const x = 10;
eval("x = x + 5");
console.log(x); // 15The string "x = x + 5" was not part of the original source code. It was created at runtime and injected into the running program.
This modifies the local scope directly, which means variables can be created, overwritten, or deleted by a string produced anywhere in your application.
The dangers are severe:
- Code injection: If any part of the eval string comes from user input, an attacker can run arbitrary code on your server or in another user's browser.
- Performance: JavaScript engines apply heavy optimizations based on static analysis. Calling eval forces the engine to throw away those optimizations for the entire containing function because it cannot predict what the string will do.
- Debugging: Stack traces, breakpoints, and source maps cannot reason about dynamically generated code.
- Scope pollution: eval can introduce new variables into local scope, making it impossible to reason about what names exist where.
// Never do this: user input in eval
const userExpression = getUserInput();
eval(userExpression); // Attacker can run anythingThe only defensible use of eval today is in build tools and bundlers that run at compile time, never in browser or server code that handles real traffic.
The Function Constructor -- Safer Dynamic Code
The Function constructor also creates code from a string, but with a critical difference: it runs only in the global scope:
const add = new Function("a", "b", "return a + b;");
console.log(add(5, 3)); // 8Because the created function executes in global scope, it cannot access or modify local variables. This makes it safer than eval for isolation, but it still carries injection risks and debugging difficulties.
The Function constructor appears in legitimate tools. Module bundlers use it to wrap chunks of code.
Template engines sometimes compile templates into functions for speed, and testing frameworks use it to create isolated test contexts:
// Template engine compiling a template into a reusable function
const template = "return `Hello, ${name}!`";
const greet = new Function("name", template);
console.log(greet("Alex")); // Hello, Alex!Even in these cases, the input should come from trusted sources only. Never pass raw user input to the Function constructor.
Monkey-Patching -- Replacing Behavior on Existing Objects
Monkey-patching means replacing a method on an existing object or prototype after it was originally defined:
// Original behavior
const api = {
fetch(url) {
console.log(`Fetching ${url} from real server`);
}
};
// Monkey-patch for testing
const originalFetch = api.fetch;
api.fetch = function (url) {
console.log(`Intercepted: returning mock for ${url}`);
return { data: "mock" };
};
api.fetch("/users"); // Intercepted: returning mock for /usersThis is a form of self-modification: the program replaced its own fetch method at runtime. Monkey-patching is common in testing, where mocking libraries replace network calls, file reads, or timer functions. It also appears in polyfills, which add missing methods to built-in prototypes.
The risks are real but manageable:
- Patches that forget to restore the original can leak between tests.
- Two libraries patching the same method can conflict.
- Debugging becomes harder because the running code does not match the source.
When you monkey-patch, always save and restore the original:
function withMock(original, mockFn, action) {
try {
api.fetch = mockFn;
return action();
} finally {
api.fetch = original;
}
}Proxy API -- Structured Self-Modification
The Proxy API is the safest and most intentional form of self-modifying behavior in JavaScript. Instead of rewriting source code or injecting strings, you define a handler object with traps that intercept specific operations:
const target = { count: 0 };
const handler = {
get(obj, prop) {
console.log(`Accessing property "${prop}"`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`Setting "${prop}" from ${obj[prop]} to ${value}`);
obj[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
proxy.count = 5; // Setting "count" from 0 to 5
console.log(proxy.count); // Accessing property "count" → 5The original target object never changes its interface. The proxy wraps it and intercepts every get and set operation. The behavior is self-modifying in the sense that the program defined new operational logic at runtime, but it is fully structured: every trap is explicit, and no code strings are involved.
For a complete walkthrough of Proxy traps and revocation, see /javascript/advanced-javascript-proxies-complete-guide.
When Self-Modification Makes Sense
Self-modifying code is not inherently bad. It is a tool that fits specific high-value use cases:
| Use case | Mechanism | Why it fits |
|---|---|---|
| Hot Module Replacement | Function constructor + module cache | Bundlers like Webpack swap module implementations at runtime without a full page reload |
| Test mocking | Monkey-patching or Proxy | Replacing network, file system, or timer APIs so tests run fast and deterministically |
| Dependency injection | Proxy | Frameworks create proxy objects that lazily resolve dependencies instead of instantiating everything upfront |
| ORM query builders | Proxy | Libraries like Sequelize return proxy objects from Model.findAll() that only execute the query when awaited or iterated |
| Reactive state | Proxy | Vue 3 and MobX use Proxy to track property access and automatically re-render components when state changes |
| Access control | Proxy | Wrapping sensitive objects with a proxy that throws on unauthorized property access |
The common thread is that these are infrastructure patterns, not application logic. You benefit from them when using frameworks, but you rarely need to write them yourself in feature code.
When to Avoid It and What to Use Instead
If you find yourself reaching for eval or monkey-patching in application code, there is almost always a safer alternative:
| Instead of | Use this |
|---|---|
eval() for dynamic expressions | A lookup table of pre-defined functions |
| Monkey-patching built-ins | A wrapper module that re-exports with custom behavior |
| Generating code strings | The strategy pattern: pass functions, not strings |
| Rewriting methods at runtime | Configuration objects or feature flags |
| Self-modifying classes | Composition: wrap behavior in separate objects |
For example, instead of building a dynamic expression with eval():
// Risky: eval-based calculator
function calculate(expression) {
return eval(expression);
}This function looks harmless, but expression could contain anything, including code that reads cookies or calls a network API. A caller never has to pass a math expression at all.
A constrained lookup table removes that risk entirely. It only allows the exact operations you define, so there is no string to inject:
// Safe: lookup table
const operations = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b
};
function calculate(op, a, b) {
return operations[op](a, b);
}
console.log(calculate("add", 5, 3)); // 8The lookup table approach is predictable, debuggable, and immune to injection. You trade flexibility for safety, and in nearly all application code, that is the right trade.
The Role in JavaScript Engines
Self-modifying code creates a challenge for JavaScript engines. Modern engines like V8 make assumptions about your code after parsing it. They build hidden classes, inline caches, and optimized machine code based on those assumptions.
When code self-modifies, the engine must deoptimize: throw away compiled assumptions and fall back to a slower, interpreter-based execution path. Strict mode removes some of this cost by banning the with statement outright and by keeping variables created inside eval() out of the surrounding scope, so the engine has fewer unknowns to guard against.
For more on how V8 handles optimization and deoptimization, see /javascript/turbofan-compiler-and-js-optimization-guide and /javascript/javascript-jit-compilation-advanced-tutorial.
Rune AI
Key Insights
- Self-modifying code alters its own logic, structure, or behavior while the program is running.
- JavaScript supports this through eval(), the Function constructor, Proxy traps, and runtime monkey-patching.
- eval() is the riskiest mechanism because it executes in the local scope and disables engine optimizations.
- The Proxy API is the safest form of self-modification because it intercepts operations without rewriting source code.
- Most self-modifying patterns have safer alternatives: use higher-order functions, configuration objects, or the strategy pattern instead.
Frequently Asked Questions
Is self-modifying code ever a good idea in production?
Why is eval() considered dangerous?
Conclusion
Self-modifying code is a powerful metaprogramming technique that JavaScript supports through several built-in mechanisms. While tools like Proxy and the Function constructor have legitimate uses in frameworks and testing libraries, most application code should avoid self-modification. When you feel the urge to generate code at runtime, ask whether a plain function, a configuration object, or a well-known design pattern can solve the problem without sacrificing readability and safety.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.