Ananya, a fresher in Chennai, is two weeks into her first job as a junior front-end developer. Like most beginners, she pictures the JavaScript engine as a reader that starts at line 1 and works its way down, one line at a time. For most code that picture gives the right answer. Then, during a code review, a senior teammate shows her two lines where console.log(a) comes before var a = 10 and asks her to guess the output. Ananya says it will fail, because a does not exist yet. The console prints undefined. So how did the engine know about a before it reached that line?
console.log(a); // undefined
var a = 10;
If JavaScript only read line by line, line 1 should fail, because a has not been created yet. Instead it prints undefined, which means the engine already knew that a exists. The reason is that JavaScript does some preparation work before it runs your code, and the place where that preparation happens is called an execution context.
In Chapter 7 we looked at objects and arrays, the data your program works with. This chapter is about what the engine does with your code itself. It is the foundation for the next few chapters: the call stack, hoisting, scope and closures all make much more sense once you know what an execution context holds and when it is created.
What Is an Execution Context?
An execution context is the environment in which a piece of JavaScript code runs. It is the engine's record of everything it needs while that code is running: which variables and functions exist, what values they hold right now, where to look for names that are not found locally, and what the keyword this means.
A restaurant is a useful picture. Before the first customer walks in, the staff arrange the tables, prepare the ingredients, open the cash counter and switch on the kitchen. Only then does service start. JavaScript works the same way. It first sets up a workspace for your code, and only after that does it start running the statements. Even a one-line program such as console.log("Hello World"); gets this workspace, because no JavaScript code can run outside an execution context.
Each execution context holds three main things. Keep these in mind, as the rest of the chapter fills them in one by one:
| Part | Plain meaning | Example |
|---|---|---|
| Memory (variables and functions) | A list of every name declared in this piece of code and its current value | price → 200, total → function |
| Outer reference | A link to the place where this code was written, so the engine knows where to look for names it cannot find locally | A function written at the top level links to the global context |
this | The value of the keyword this while this code runs | window in a browser script |
Types of Execution Context
JavaScript has three kinds of execution context. You will work with the first two every day.
- Global execution context. Created once, when your script or file starts. All top-level code (code that is not inside any function) runs here. There is only one per script, and it stays around until the program ends or the page is closed.
- Function execution context. Created every time a function is called, not when it is defined. Call the same function five times and you get five separate contexts, one after another.
- Eval execution context. Created for code passed to
eval(), which turns a string into running code. Modern code avoidseval()because it is slow, hard to debug and a security risk, so we will not spend time on it.
In the newer standard, JavaScript modules (files loaded with import) also get their own top-level context. We will come back to that when we look at this.
The Two Phases: Creation and Execution
Every execution context, global or function, is built in two phases. This is the single most useful idea in the chapter, and most "strange" JavaScript behaviour is explained by it.
- Creation phase (also called the memory phase). The engine scans the code for declarations and sets aside memory for every variable and function it finds. No line of your code runs yet.
- Execution phase. The engine now runs the code from top to bottom. Assignments fill in real values, functions get called, and output gets printed.
The important detail is what gets stored in the creation phase. It depends on how the name was declared, and the three kinds behave very differently:
| Declaration | Stored in the creation phase as | Using it before its line |
|---|---|---|
var x = 10; | x → undefined | Gives undefined, no error |
let x = 10; / const x = 10; | x is registered but uninitialised | ReferenceError: Cannot access 'x' before initialization |
function greet() { ... } | greet → the complete function | Works; you can call it before its line |
var greet = function () { ... }; | greet → undefined (it is just a var) | TypeError: greet is not a function |
The gap between the start of the context and the line where a let or const is declared has a name: the temporal dead zone, or TDZ. The variable exists in memory during that gap, but JavaScript refuses to let you read or write it. This is deliberate. Reading a value that is not ready yet is almost always a bug, and the TDZ turns that bug into a clear error instead of a quiet undefined.
Ananya tries this the next morning. She changes var a = 10 to let a = 10 in the same two-line snippet, and the quiet undefined becomes ReferenceError: Cannot access 'a' before initialization. The variable still exists from the start of the context, but now the engine refuses to hand out a value that is not ready. For her, that error is good news: a bug that stops the program on the exact line is far easier to find than one that quietly prints undefined.
Here is all of it in one runnable file. Each line's output is shown as a comment:
console.log(a); // undefined
var a = 10;
console.log(a); // 10
try { console.log(b); } catch (e) { console.log(e.message); }
// Cannot access 'b' before initialization
let b = 1;
console.log(sayHello()); // Hello
function sayHello() { return "Hello"; }
try { sayHi(); } catch (e) { console.log(e.message); }
// sayHi is not a function
var sayHi = function () { return "Hi"; };
try { sayBye(); } catch (e) { console.log(e.message); }
// Cannot access 'sayBye' before initialization
const sayBye = () => "Bye";
Notice the difference between the last two. sayHi was declared with var, so in the creation phase it got undefined, and calling undefined() gives a TypeError. sayBye was declared with const, so it was still in the TDZ, and you get a ReferenceError instead. Only a real function declaration is stored whole and can be called early. This "available before its line" behaviour has its own name, hoisting, and Chapter 10 covers it in detail.
A Gotcha: Function and var With the Same Name
Because function declarations are stored whole in the creation phase and var assignments only happen in the execution phase, a name used for both can change type halfway through the program:
console.log(typeof greet); // function
var greet = "hi";
function greet() {}
console.log(typeof greet); // string
In the creation phase, the function wins: var greet does not overwrite a function that is already stored. In the execution phase, the line greet = "hi" runs and replaces it with a string. With let or const this code would not even load, because they refuse a duplicate name in the same scope. That is one more reason to prefer them.
The Global Execution Context
As soon as your file starts, the engine creates the global execution context and runs its creation phase over all top-level code. Think of it as the head office of your program: every function call later starts from here and returns here.
In the creation phase, the global context also sets up two things you did not write yourself. The first is the global object, which holds built-in features. In a browser it is called window, in Node.js it is called global, and both can be reached with the standard name globalThis. The second is the value of this for top-level code, which surprisingly depends on where the code runs.
this in the Global Context
| Where the top-level code runs | Value of this |
|---|---|
Browser, classic <script> | window (the global object) |
Browser, <script type="module"> | undefined |
Node.js, CommonJS file (.js with require, or .cjs) | module.exports, which starts as an empty object {} |
Node.js, ES module (.mjs, or "type": "module" in package.json) | undefined |
The Node.js rows are easy to check yourself. Save this as a .js file and run it with node:
// this-check.js (CommonJS)
console.log(this); // {}
console.log(this === module.exports); // true
module.exports.answer = 42;
console.log(this); // { answer: 42 }
Node wraps each CommonJS file in a hidden function and calls it with this set to module.exports, so the empty object you see is really the file's export object. Rename the same file to this-check.mjs and the first line prints undefined, because ES modules always run in strict mode with no global this. If you are not sure which module system your project uses, the CommonJS vs ES Modules post explains the difference.
Global var vs let in the Browser
In a classic browser script, top-level var variables and function declarations become properties of window. Top-level let and const do not; they live in a separate area of the global context.
// In a classic browser <script>
var a1 = 1;
let b1 = 2;
function f1() {}
console.log(window.a1); // 1
console.log(window.b1); // undefined
console.log(typeof window.f1); // function
This is one reason global var is risky in the browser: your variable can accidentally overwrite a built-in such as window.name or a variable from another script. In a Node.js file, and in any module, top-level var is local to that file and does not touch globalThis at all.
The Function Execution Context
Every function call gets a brand-new execution context, and it goes through the same two phases. Think of it as a temporary room the head office books for one job. The room is set up, the job is done, and the room is handed back.
The creation phase of a function context does a little more work than the global one:
- Parameters are stored with the values passed in the call. Missing arguments get
undefined. - For regular functions (not arrow functions), an
argumentsobject is created that holds every value passed, even extra ones. - Local
var,let,constand function declarations are registered by the same rules as in the global context. thisis decided by how the function was called. Arrow functions are the exception: they do not get their ownthisand use the one from the code around them.- The outer reference is set to the place where the function was written.
function sum(a, b) {
console.log(arguments.length, a, b);
}
sum(1, 2, 3); // 3 1 2
sum(1); // 1 1 undefined
When the function returns, its context is removed and control goes back to the context that called it. Because each call starts from scratch, local variables do not remember anything between calls:
function visit() {
var count = 0;
count++;
return count;
}
console.log(visit()); // 1
console.log(visit()); // 1 (a fresh count every call)
The engine keeps track of which context is running, and which one to return to, with a structure called the call stack. The global context sits at the bottom, each call is pushed on top, and each return pops it off. Chapter 9 is all about it.
Local Variables Stay Local
function test() {
let secret = "Hidden";
console.log(secret);
}
test(); // Hidden
console.log(secret); // ReferenceError: secret is not defined
It is often said that this error happens "because the function's context has been destroyed". That is not the full reason. Code in the global context could never see secret, even while test() was still running, because lookup only goes outwards from where the code is written, never inwards into a function. The next section shows how that lookup works.
Variable Environment, Lexical Environment and the Scope Chain
So far we have called the memory part of a context its "memory". The specification has more precise names, and you will meet them in interviews and in other articles, so it helps to know what they mean in simple words.
- Environment record: the actual table of names and values, like
price → 200. - Lexical environment: an environment record plus a link to the outer environment. "Lexical" just means "based on where the code is written in the file".
- Variable environment: the lexical environment that holds the context's
vardeclarations. When a function starts, both names point to the same place. They only separate when a block such as{ },iforforcreates a small new lexical environment for itsletandconst, whilevarstays in the function's variable environment.
That last point explains a classic difference between var and let. A block gives let its own small environment, but it does not create a new execution context, so var inside a block belongs to the surrounding function or global code:
var x = 1;
let y = 1;
{
var x = 2; // same x as above
let y = 2; // a new y that lives only in this block
}
console.log(x, y); // 2 1
When code uses a name, the engine first looks in the current environment. If the name is not there, it follows the outer reference to the next environment, then the next, until it reaches the global one. If the name is still not found, you get a ReferenceError. This chain of links is the scope chain.
const city = "Delhi";
function outer() {
const shop = "Kirana";
function inner() {
const item = "rice";
console.log(item, shop, city);
}
inner();
}
outer(); // rice Kirana Delhi
Inside inner, item is found locally, shop is found one step out in outer, and city is found two steps out in the global environment. The key word is written. The outer reference is fixed by where the function sits in the file, not by who calls it:
const city = "Delhi";
function showCity() {
console.log(city);
}
function trip() {
const city = "Mumbai";
showCity();
}
trip(); // Delhi
showCity is called from inside trip, but it was written at the top level, so its outer reference points to the global environment and it prints "Delhi". It never looks inside trip. This rule is called lexical scoping, and it is exactly what makes closures possible in Chapter 12.
Step-by-Step Trace of a Small Program
Let's follow a short bill calculator through both phases of both contexts. It works out the total for 3 items at ₹200 each, plus 18% tax:
var price = 200;
let qty = 3;
function total(p, q) {
var tax = p * q * 0.18;
return p * q + tax;
}
var bill = total(price, qty);
console.log(bill); // 708
The diagram below shows the two contexts at the moment total is running. The global context waits underneath while the function context does its work, and the function's outer reference points back to the global memory.
Here is every step, in order. The "Memory after this step" column shows only what changed:
| # | Context and phase | What happens | Memory after this step |
|---|---|---|---|
| 1 | Global, creation | Engine scans top-level code; sets this and the global object | price → undefined, qty → uninitialised (TDZ), total → function, bill → undefined |
| 2 | Global, execution | var price = 200; | price → 200 |
| 3 | Global, execution | let qty = 3; (TDZ ends here) | qty → 3 |
| 4 | Global, execution | The function declaration is skipped; it was stored in step 1 | No change |
| 5 | Global, execution | total(price, qty) is called; global pauses | No change yet; bill is still undefined |
| 6 | total, creation | New function context; parameters, arguments, this and outer reference set up | p → 200, q → 3, tax → undefined |
| 7 | total, execution | var tax = p * q * 0.18; | tax → 108 |
| 8 | total, execution | return p * q + tax; gives 600 + 108 | Returns 708; the function context is removed |
| 9 | Global, execution | Back in global; the returned value is assigned | bill → 708 |
| 10 | Global, execution | console.log(bill); prints 708 | No change; the program ends |
Two things are worth noticing. First, total never had its own price or qty; it only knew p and q, the copies passed in as arguments. Second, at step 5 the global context is not finished, only paused. It resumes at exactly the same spot when the function returns, which is the job of the call stack.
Calling two functions one after the other follows the same pattern twice. With one(); two();, the global context creates a context for one, removes it when one returns, then creates a fresh one for two:
Global context (running)
│
├── one() called → context for one created → prints "One" → removed
│
├── two() called → context for two created → prints "Two" → removed
│
Global context continues until the end of the file
"Removed" Does Not Always Mean "Forgotten"
Most tutorials say a function context is "destroyed" when the function returns. That is true for the context itself: it is popped off the call stack and will never run again. Its variables, though, are only freed when nothing else needs them. If an inner function that uses those variables is still around (because it was returned, or saved as an event handler), the variables are kept alive for it. That is a closure, and it is why a function can "remember" values after its parent has finished. You only need to know that it exists for now; Chapter 12 explains it properly.
Where This Shows Up in Real Apps
Every user action in an app usually triggers at least one function call, and therefore at least one new execution context. When you tap "Pay" in a UPI app, a function like transferMoney() runs in its own context with its own amount, receiver and status variables. A food delivery app might call calculateDeliveryCharge(distance) for each restaurant in the list, and each of those calls gets a separate context, so the distance of one restaurant never leaks into another. The same is true for loginUser() and placeOrder() on a shopping site.
This isolation is what lets thousands of calls to the same function run safely in one program. Problems start only when code reaches outside its context, for example by changing a global variable. Then two calls that should be independent start affecting each other, which is one of the hardest kinds of bug to trace.
Common Mistakes (and the Fix)
| Mistake | What happens | Fix |
|---|---|---|
Using a var before its line and expecting an error | You silently get undefined and the bug shows up later | Use let/const, which throw a clear ReferenceError in the TDZ |
| Calling a function expression before its line | TypeError: ... is not a function (with var) or ReferenceError (with let/const) | Define it above the call, or use a function declaration if early calls are needed |
Assuming typeof is always safe | typeof x throws if x is a let/const still in its TDZ | Declare variables at the top of their scope |
| Expecting a function to see the caller's variables | It sees variables from where it was written, not from where it was called | Pass the values you need as arguments |
Relying on top-level this | It is window, {} or undefined depending on the environment | Use globalThis for the global object and module.exports/export for exports |
Using a var and a function with the same name | The name changes type halfway through the program | Give every declaration a unique name; let/const enforce this |
The typeof case surprises people, because typeof on a name that was never declared is completely safe and returns "undefined". Only a name that is declared later with let or const throws:
console.log(typeof neverDeclared); // undefined
try { typeof later; } catch (e) { console.log(e.message); }
// Cannot access 'later' before initialization
const later = 5;
My Opinion
Learn the two-phase model, then mostly forget about it while writing code. It is the best tool for explaining confusing output and for interviews. In everyday code, the goal is to write things in an order where the creation phase never surprises anyone.
Use const by default and let when you must reassign. The TDZ is a feature: it turns "used before it was ready" into an error you see immediately, instead of a quiet undefined that breaks something three screens later. There is very little reason to write var in new code.
Declare things before you use them, even when JavaScript would allow otherwise. Calling a function declaration above its definition works, but code that reads top to bottom is easier for the next person. Many teams turn on the ESLint no-use-before-define rule for this reason, and it is a sensible default.
Keep the global context small. Every top-level variable is visible to everything in the file, and in classic browser scripts var even lands on window. Put logic inside functions or modules so each piece of work gets its own isolated context.
Use the debugger to see contexts for real. Put a breakpoint inside a function in Chrome DevTools or VS Code and look at the "Scope" and "Call Stack" panels. You will see the local, closure and global environments listed exactly as described here, which makes the idea stick far better than any diagram.
Try It Yourself
Predict the output of each snippet before running it with Node.js or in your browser console (press F12 → Console). Answers are below.
1. console.log(n);
var n = 5;
2. console.log(m);
let m = 5;
3. console.log(double(4));
function double(x) { return x * 2; }
4. console.log(triple(4));
var triple = function (x) { return x * 3; };
5. var color = "red";
function show() { console.log(color); }
function paint() { var color = "blue"; show(); }
paint();
6. var k = 1;
{ var k = 2; }
console.log(k);
7. function add(a, b) { return arguments.length; }
console.log(add(1, 2, 3, 4));
8. // saved as test.js and run with: node test.js
console.log(this);
Show answers
1. undefined → var is stored as undefined in the creation phase
2. ReferenceError: Cannot access 'm' before initialization → m is in the TDZ
3. 8 → function declarations are stored whole, so the early call works
4. TypeError: triple is not a function → triple is a var holding undefined
5. red → show was written at the top level, so it looks in global
6. 2 → a block does not create a new context; var k is the same variable
7. 4 → arguments holds every value passed, not just the parameters
8. {} → in a CommonJS file, top-level this is module.exports
Frequently Asked Questions
Is execution context the same as scope?
No, but they are closely linked. Scope is the set of names your code can see, and it is fixed by where the code is written. An execution context is created at run time when code actually runs, and it carries the lexical environment that makes that scope work, plus this.
How many execution contexts can exist at once?
One global context, plus one for every function call that has started but not yet returned. Nested calls stack up, so if a() calls b() which calls c(), four contexts exist together. Too many (for example from runaway recursion) gives the "Maximum call stack size exceeded" error covered in Chapter 9.
Do arrow functions get an execution context?
Yes. Every call to an arrow function creates a function context with its parameters and local variables. The difference is that arrow functions do not get their own this or arguments; they use the ones from the surrounding code.
Does an if block or a for loop create an execution context?
No. Blocks create a new lexical environment for let and const, which is why those variables stay inside the block. They do not create a new execution context, so var inside a block belongs to the surrounding function or to the global code.
Key Takeaways
- An execution context is the environment in which code runs: its variables and functions, an outer reference, and
this. - There is one global context per script, and a new function context for every function call.
- Each context has a creation phase (declarations set up) followed by an execution phase (code runs line by line).
- In the creation phase,
varbecomesundefined,let/conststay uninitialised in the TDZ, and function declarations are stored whole. - The outer reference is fixed by where a function is written; following these links is the scope chain.
- Top-level
thisiswindowin a classic browser script,{}(module.exports) in a CommonJS file, andundefinedin ES modules. - Blocks create a lexical environment for
let/constbut not a new execution context. - A finished function's context is removed, but its variables survive if a closure still uses them.
Next, Chapter 9 shows how the engine stacks these contexts on top of each other with the call stack.
References
- ECMAScript specification: Execution Contexts
- MDN: let and the temporal dead zone
- MDN: this
- MDN: Hoisting

0 Comments