Execution context, the call stack, and the hoisting trap
Every interviewer at some point asks, "what happens when JavaScript runs your file?" The honest answer is, it runs it twice. First a memory pass, then a code pass. If you have never thought of it that way, hoisting feels like magic. Once you do, every trap question becomes obvious.
This lesson is the foundation. Closures, this, the event loop, all of them sit on top of execution contexts. Get this clear and the rest of senior JS clicks.
What an execution context is
An execution context (EC) is the box JavaScript creates every time it runs some code. There are two kinds.
- Global execution context. Created once when your file starts. Sets up
window(orglobalThis),this, and scans your top-level variables. - Function execution context. Created fresh every time a function is called. Sets up
arguments, the function's own variables, and its ownthis.
Each EC runs in two phases.
- Memory creation phase. JS scans the code, allocates space for variables and functions.
vargetsundefined.letandconstget a slot but no value (Temporal Dead Zone). Function declarations get their full body. - Execution phase. JS runs the code line by line, top to bottom.
That two-pass behaviour is the whole reason hoisting exists. JS already knows your variable names before it executes line 1.
The call stack, a stack of plates
JS is single-threaded. It can only do one thing at a time. The call stack is how it remembers which function it is currently inside.
Picture a stack of plates at a Bengaluru thali counter. Every function call adds a plate on top. Every return removes the top plate.
function placeOrder() {
chargeUPI(499);
}
function chargeUPI(amount) {
console.log("Debiting", amount);
}
placeOrder();Step-by-step execution.
- Step 1. Global EC is pushed. JS scans, sees
placeOrderandchargeUPIas full function bodies. - Step 2. Hits
placeOrder(). New EC forplaceOrderis pushed onto the stack. - Step 3. Inside it,
chargeUPI(499)is called. A third EC pushed. - Step 4.
console.logruns,chargeUPIreturns, its EC is popped. - Step 5.
placeOrderreturns, popped. Global EC sits alone until the script ends.
Interview trap: if you ever see "Maximum call stack size exceeded", you have infinite recursion. The stack grew without ever popping.
Hoisting, the part that bites you
Hoisting is just the memory phase doing its job. It is not magic, it is not "moving code to the top". JS simply allocated the names before running line 1.
console.log(price); // undefined, not ReferenceError
var price = 199;
console.log(qty); // ReferenceError: Cannot access 'qty' before initialization
let qty = 2;
greet(); // "Namaste" — works
function greet() { console.log("Namaste"); }The three rules.
varis hoisted and initialised asundefined. That is whyconsole.log(price)printsundefinedand not an error.letandconstare hoisted but uninitialised. They sit in the Temporal Dead Zone until the line that declares them runs. Touch them earlier and you get a ReferenceError.- Function declarations are fully hoisted. The whole function body is in memory before line 1, which is why you can call
greet()above its definition.
Interview trap:
varfunction expressions do NOT hoist the function body.console.log(add); var add = function(a,b){return a+b}printsundefined, then errors if you try to calladd(1,2)on the next line.
Quick reference
- Execution Context
- The environment JS creates to run a piece of code, with its own variables, scope, and this.
- Call Stack
- LIFO data structure JS uses to track which function is currently executing.
- Hoisting
- The effect of the memory creation phase: variable and function names are known before code runs.
- Temporal Dead Zone
- The gap between a let or const being hoisted and the line that initialises it. Accessing the variable in this gap throws.
Predict the output
console.log(x);
console.log(food);
var x = 5;
let food = "biryani";Step by step.
- Memory phase.
xis set toundefined,foodis hoisted into the TDZ. - Execution phase. Line 1 prints
undefined. Line 2 throwsReferenceError: Cannot access 'food' before initialization.
The script never even reaches var x = 5.
Senior rule: JS does two passes per execution context, memory first, then code. Hoisting is not weird. It is just the memory pass leaking into your mental model.
If you can draw the call stack on a whiteboard and label each EC with its variables and their hoisted state, you have already beaten 80% of interview candidates on this topic.
Quick quiz, prove you got it
- 1.What does `console.log(x); var x = 5;` print?
- 2.What about `console.log(x); let x = 5;`?
- 3.Which is fully hoisted, body and all?
Interview insights
When asked "what is hoisting?"
- Mention the two-pass model: memory creation phase, then execution phase
- Distinguish var (initialised undefined) from let/const (in TDZ)
- Mention that function declarations are fully hoisted but expressions are not
takeaway: Show the two phases, not just the catchphrase "hoisting moves declarations to the top".
Tricky questions they will ask
Q.Is hoisting actually "moving code to the top"?›
A.No. Nothing physically moves. JS does a pass before execution where it scans for declarations and reserves memory. The actual code order in your file does not change.
Q.What is the order of two function declarations with the same name?›
A.The last one wins. During the memory phase, JS overwrites the earlier reference with the later one. Both declarations get processed before any code runs.
Free tools you can use while you learn
Common questions
Q.What is the difference between hoisting var and let?›
Q.What is the call stack used for?›
Watching quietly. Tap me if you want a tip.
Try this (0 of 2 done)
- 1
Make the first log print undefined (not throw) by using var, then assign.
show answer
console.log(name); var name = 'Aarav'; console.log(name); - 2
Trigger a ReferenceError by trying to log a let before its declaration. Wrap in try/catch to see the error message.
hint
let is in the Temporal Dead Zone until declared.
show answer
try { console.log(x); } catch(e) { console.log(e.message); } let x = 5;