eval & new Function
Learn what eval and new Function do with code strings, how direct and indirect eval choose scope, why CSP can block them, and safer patterns to reach for instead.
- 01Explain the scope ruleTell direct eval, indirect eval, and new Function apart.
- 02Spot the dangerDescribe why code strings are a security and CSP problem.
- 03Choose safer toolsReplace data, property, and parsing eval patterns with ordinary APIs.
Strings are not programs until you compile them
JavaScript normally runs source code that came from a file, a module, or a script tag. eval and new Function are different: they take text while the program is already running and ask the engine to parse that text as JavaScript.
That sounds flexible, but it changes the rules around scope, closures, strict mode, browser security, and performance. This lesson shows the real behavior, then spends most of its time on the more useful skill: replacing code strings with ordinary data and functions.
Treat code strings like hazardous material. If you only need data, parse data. If you need to choose behavior, choose from functions you already wrote.
Direct eval is like handing a stranger a note and saying, “do whatever this says, with full access to this room.” Indirect eval is the same note read in the building lobby: it can use lobby signs, but not the private labels on your desk.
- In real life: The room you are standing in
- In JavaScript: The current function scope
- In real life: A note that says what to do
- In JavaScript: The string passed to
eval - In real life: Reading it inside the room
- In JavaScript: Direct
eval(...)can see local variables - In real life: Reading it in the lobby
- In JavaScript: Indirect eval sees only global names
Where the analogy stops: A real note cannot magically create a variable or read a closure. JavaScript code can, depending on the eval form, so the analogy is only about trust and location.
Direct vs indirect eval
STEP THROUGHA call is direct eval only when the source is exactly eval(...) and the name still refers to the original global eval function. Direct eval runs in the caller’s scope, so the string can read local variables. Anything else is indirect eval: an alias, a comma expression, optional chaining, or a property access. Indirect eval runs as global code.
Pick a mode, predict which scope supplies x, then step through the model. This is a guided replay, not an engine debugger.
script
const x = "local x"; return eval("x");}console.log(readX());The distinction is intentionally narrow. (0, eval)("x") looks silly, but it is a common way to force indirect eval because the comma expression returns the function value instead of making a direct call through the identifier eval.
const cases = { direct: 'function read(){ const x = "local x"; return eval("x"); } read();', indirect: 'globalThis.x = "global x"; function read(){ const x = "local x"; return (0, eval)("x"); } read();', sloppyVar: 'function read(){ eval("var leaked = 1"); return typeof leaked; } read();', strictVar: 'function read(){ "use strict"; eval("var hidden = 1"); return typeof hidden; } read();'};addEventListener("message", (event) => { if (!event.data || event.data.lesson !== "eval" || !(event.data.caseId in cases)) return; try { const value = eval(cases[event.data.caseId]); parent.postMessage({ lesson: "eval", caseId: event.data.caseId, status: "ok", value: String(value) }, "*"); } catch (error) { parent.postMessage({ lesson: "eval", caseId: event.data.caseId, status: "error", value: error.name + ": " + error.message }, "*"); }});Preparing the sandboxed frame…
choose a snippetThe parent sends only a case ID. The sandbox contains the fixed snippets and reports back after the parent validates event.source. There is no learner-typed code path.
Declarations inside eval
BROWSER LABReading variables is only half the story. A sloppy direct eval can also create var bindings in the surrounding variable environment. Strict direct eval is safer: it gets its own variable environment, so var declarations inside the string do not leak out.
function sloppy() { eval("var leaked = 1"); return typeof leaked;}function strict() { "use strict"; eval("var hidden = 1"); return typeof hidden;}In the iframe lab above, the sloppy case reports "number" because leaked exists after eval. The strict case reports "undefined" because hidden stayed inside eval. Modern code should be strict, but the best answer is still to avoid the code string.
eval only compiles strings. eval(42) returns 42, and eval(object) returns the same object unchanged.
new Function builds a fresh global-scope function
INTERACTIVEThe Function constructor builds a brand-new function from text. It can use the parameters you feed it and any global names in the lobby, but it never knows the local variables in the room where you created it.
- In real life: Buttons for ingredients
- In JavaScript: Parameter names like
aandb - In real life: A recipe printed by the machine
- In JavaScript: The function body string
- In real life: The building lobby
- In JavaScript: Global scope
- In real life: Locked offices upstairs
- In JavaScript: Local variables and closures it cannot see
Where the analogy stops: A vending machine cannot execute arbitrary instructions; new Function can. The analogy is about receiving inputs and knowing only the lobby.
globalThis.shared = 7;const add = new Function("a", "b", "return a + b");function makeSecretReader() { const secret = 10; return new Function("return secret");}const readSecret = makeSecretReader();const readShared = new Function("return shared");console.log(add(2, 3));try { console.log(readSecret()); }catch (error) { console.log(error.name); }console.log(readShared());const cases = { add: () => new Function("a", "b", "return a + b")(2, 3), closure: () => { const secret = 10; return new Function("return secret")(); }, global: () => { globalThis.shared = 7; return new Function("return shared")(); }};addEventListener("message", (event) => { if (!event.data || event.data.lesson !== "eval-new-function" || !(event.data.caseId in cases)) return; try { parent.postMessage({ lesson: "eval-new-function", caseId: event.data.caseId, status: "ok", value: String(cases[event.data.caseId]()) }, "*"); } catch (error) { parent.postMessage({ lesson: "eval-new-function", caseId: event.data.caseId, status: "error", value: error.name + ": " + error.message }, "*"); }});Preparing the sandboxed frame…
choose a casenew Function can use its parameters and globals. It cannot see the local secret where the function was constructed.
The body is sloppy unless the body string starts with "use strict". Like eval, new Function is blocked by a browser CSP that omits unsafe-eval.
Security and Content Security Policy
BROWSER LABThe biggest practical problem is security. If untrusted text reaches eval, the attacker is no longer choosing a value; they are choosing instructions. That is why many production sites use a Content Security Policy that does not include unsafe-eval.
A CSP without unsafe-eval is like a building rule that forbids reading instructions from notes at all. It blocks friendly notes and hostile notes the same way.
- In real life: A rule at the front desk
- In JavaScript: Content Security Policy
- In real life: No reading instruction notes
- In JavaScript: No
unsafe-eval - In real life: A guard refusing the note
- In JavaScript: Browser throws
EvalError
Where the analogy stops: CSP is not a human guard and it does not understand your intent. It applies mechanical rules to script execution and string compilation.
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'"><script>try { const value = eval("21 * 2"); parent.postMessage({ lesson: "eval-csp", status: "ok", value: String(value) }, "*");} catch (error) { parent.postMessage({ lesson: "eval-csp", status: "error", value: error.name + ": " + error.message }, "*");}</script>Preparing the sandboxed frame…
waitingwaitingBoth frames allow their inline script to start. Only the policy with unsafe-eval lets the eval call compile its string. Chrome throws an EvalError when the other frame tries.
In Chrome, the blocked frame throws an EvalError. The exact message text is browser-specific, so production code should not depend on the wording. The same policy also blocks new Function and string arguments to setTimeout or setInterval.
Safer alternatives
SORTERMost eval requests are really one of three ordinary tasks: parse data, choose a known operation, or convert text into a simple value. None of those needs JavaScript source code.
const data = JSON.parse('{"score":42}');const formatters = { upper: (text) => text.toUpperCase() };const key = "upper";const page = new URLSearchParams("?page=2");console.log(data.score);console.log(formatters[key]("safe"));console.log(Number(page.get("page")));- Use JSON with
JSON.parsefor data from APIs or storage. - Use an object,
Map, orswitchstatement when a key chooses known behavior. - Use
URLSearchParams,Number(), and validation when parsing URL or form values.
- Turn an API string like
{"ok":true}into data - Read
user[name]when the key comes from a menu - Let visitors type arbitrary formulas for a calculator
- A lesson frame runs one of three predefined snippets
- A trusted build tool compiles a small generated helper
- Read
?page=2and turn it into a number
Sort each situation by the safest approach.
Performance cost
Engines optimize ordinary code by studying which variables it can read and write. Direct eval breaks that confidence: the string might mention a local name, create a sloppy var, or rely on a name that only appears in one branch. The engine has to be conservative.
There is also a runtime cost. A normal function’s source is parsed and compiled as part of loading the program. An eval string or Function-constructor body is parsed when that line runs. Timings vary by browser, device, warm-up, and the exact code, so the lesson does not pretend a single number is universal.
Do not replace eval because a microbenchmark said one snippet was slower. Replace it because normal functions and data structures are clearer, safer, easier to optimize, and compatible with stricter CSPs.
Where you will use this
You are unlikely to write eval in application code, but you will review code that tries to. Search for string-to-code patterns during security reviews, CSP migrations, and bug hunts around surprising scope behavior.
| Tool | Scope it can read | Strictness | Common result |
|---|---|---|---|
Direct eval(code) | Caller scope plus outer scopes | Inherits caller strictness; strict direct eval keeps its own vars | Most powerful and most dangerous |
| Indirect eval | Global scope only | String decides whether it is strict | Cannot see locals |
new Function(...) | Global scope only | Sloppy unless the body says "use strict" | Creates a reusable function from text |
| JSON / lookup / parser APIs | Only the values you pass | Normal code | Preferred for data and choices |
If you build a teaching tool, rules engine, or formula editor, prefer a small parser for the exact language you need. If you truly must execute JavaScript, isolate it like the Windows, iframes & postMessage lesson: a sandbox, predefined or trusted input, timeouts where possible, and validated messages.
Common misconceptions
“Every eval call is direct eval.”
No. Only exact eval(...) is direct. Aliases and expressions are indirect.
“new Function is a safer closure eval.”
It does not see closures. It still compiles strings and is still blocked by CSP.
“JSON is JavaScript, so eval is fine.”
JSON is data. JSON.parse rejects programs; eval runs programs.
“CSP only blocks malicious strings.”
CSP cannot read intent. Without unsafe-eval, even friendly eval strings fail.
“Small eval strings are cheap.”
The engine still has to parse them at runtime and limit assumptions around scope.
Practice exercises
5 EXERCISESType the exact console output.
function demo() {
const x = "nearby";
return eval("x");
}
console.log(demo());Direct eval can read the caller’s local x, so the program prints nearby.
Type the exact console output.
globalThis.x = "global";
function demo() {
const x = "nearby";
return (0, eval)("x");
}
console.log(demo());Indirect eval ignores the local x and reads globalThis.x, so it prints global.
Predict the boolean output.
const value = { ok: true };
console.log(eval(value) === value);The object is returned unchanged, so the identity comparison prints true.
Predict the number.
globalThis.tax = 5;
function make() {
const tax = 99;
return new Function("price", "return price + tax");
}
console.log(make()(10));The generated function sees global tax = 5, so 10 + 5 prints 15.
Which API should you name when data arrives as JSON text?
const user = JSON.parse('{"name":"Ada","admin":false}');
console.log(user.name + ":" + user.admin);const user = JSON.parse(text);Use JSON.parse for JSON strings. Then read properties from the resulting object with normal JavaScript.
Quiz
7 QUESTIONSQuestion 1 of 7Which call is direct eval?
Read the code, then predictconst run = eval; eval("1 + 1"); run("1 + 1"); (0, eval)("1 + 1");Choose an answer to see the explanation.
Question 2 of 7What does direct eval read here?
Read the code, then predictfunction demo() { const x = "local"; return eval("x"); } console.log(demo());Choose an answer to see the explanation.
Question 3 of 7What does indirect eval read here?
Read the code, then predictglobalThis.x = "global"; function demo() { const x = "local"; return (0, eval)("x"); } console.log(demo());Choose an answer to see the explanation.
Question 4 of 7What does eval do with a non-string?
Read the code, then predictconst value = 42; console.log(eval(value));Choose an answer to see the explanation.
Question 5 of 7What can a function made with
new Functionsee?Read the code, then predictglobalThis.shared = 3; function make() { const shared = 9; return new Function("return shared"); } console.log(make()());Choose an answer to see the explanation.
Question 6 of 7What does a CSP without
unsafe-evalblock in browsers?Choose an answer to see the explanation.
Question 7 of 7Which replacement is best for reading a chosen object property?
Read the code, then predictconst formatters = { upper: (text) => text.toUpperCase() }; const key = "upper";Choose an answer to see the explanation.
Key takeaways
- Direct eval is exactly
eval(...)and can see the caller’s local scope. - Indirect eval and
new Functionrun in global scope;new Functiondoes not close over locals. - Strict direct eval keeps its
vardeclarations inside eval; sloppy direct eval can leak them. - CSP without
unsafe-evalblocks eval,new Function, and string timers in browsers. - Prefer
JSON.parse, lookup tables, URL parsing, validation, and ordinary functions.
Final definition.
eval and new Function turn strings into JavaScript code at runtime, with different scope rules and a security cost that usually outweighs the flexibility.
Up next: Higher-order functions.