Execution Context and Call Stack - how JavaScript runs your code

Every line of JS has to run somewhere, every variable has to live somewhere, every function call has to come back to where it was called from. The engine keeps all of this in two structures - the execution context (where am I now) and the call stack (where did I come from). They're what closure, this, and async are all built on.
Every line of JS you write has to run somewhere. Every variable - has to live somewhere. Every function call - has to come back, sooner or later, to the place it was called from. The JS engine keeps all of this in two structures: the execution context and the call stack.
You don’t have to think about them day-to-day - intuition gets you through most of your code. But quite a lot in JS rests on them, including:
- closure - a function that “remembers” variables from its old context;
this- whose value depends on how the function was called (four binding rules plus arrow functions);- the “one thing at a time” character of JS - because there’s one stack and one thread.
That’s not the full list - the whole story of async (event loop, promises, async/await), hoisting, the stack trace you see on errors, generators, even how class from ES2015 works, all comes from this same mechanism. Once you see it from the inside, the rest of JS clicks into place.
#What gets created when you run a JS file
When you run a JS file (node script.js, opening an HTML page in a browser), the engine creates a global execution context - the Global Execution Context. It comes into being once, at the start, and lives for the entire run of the program.
It has two parts:
- Thread of execution - it reads the code line by line and runs it. Synchronously, one step at a time, no interleaving.
- Memory (variable environment) - storage for variables and function declarations that live in the global.
Look at this code:
const x = 10;
function double(n) {
return n * 2;
}
const y = double(x);What happens in the global execution context, step by step:
- The thread reads
const x = 10and writes to memory:x = 10. - The thread reads the declaration
function double(n) { ... }. It creates the function as an object and writes it to memory under the namedouble. It does not step into the function - it just registers that the function exists. - The thread reads
const y = double(x). It sees a function call - and that’s where the interesting part begins.
#Function call = a new local context
When you call a function, the engine creates a new execution context - for this specific function, with these specific arguments. A local context has the same parts as the global one, just at a smaller scale:
- local memory - parameters and the function’s local variables go here;
- a local thread - which now runs the code inside the function, line by line.
Step by step for double(10):
- A new local context is created for
double. - The parameter
nlands in this context’s local memory with the value10(from the argument). - The local thread runs
return n * 2, returning20. - After
return, the local context is destroyed. - The value
20goes back to global memory asy.
And only now does the global thread pick up the next lines.
While a local context is alive, the global thread is paused. The engine doesn’t go back to the main flow of the code until the function returns. That’s why we say JS runs synchronously - the thread is in one place at a time.
#Why we need the Call Stack
Picture a function that calls another function. Or a third one. Or recursion:
function square(n) {
return n * n;
}
function sumOfSquares(a, b) {
const sa = square(a);
const sb = square(b);
return sa + sb;
}
sumOfSquares(3, 4);How does the engine know:
- where the thread of execution currently is?
- which context to return to when the current function returns?
The answer is the call stack - a data structure where the engine keeps a list of active contexts. “Stack” means:
- every new execution context is a push - dropped on top;
- every
returnis a pop - taken off the top; - at the bottom there’s always the global context (until the program ends);
- on top - the context currently running.
LIFO, last in, first out: the most recently pushed context is the first to be popped.
Here’s how it looks step by step for sumOfSquares(3, 4) from our example. Each press of Next is one engine action - a push when a function is called, a pop on return. The active context (TOP) is always highlighted. Notice how the active line of code on the left moves with it, and how the local memory of the frame grows as a new result from square lands in sumOfSquares.
function square(n) { return n * n;} function sumOfSquares(a, b) { const sa = square(a); const sb = square(b); return sa + sb;} sumOfSquares(3, 4);
Whatever is on top of the stack - that’s where the thread of execution currently sits. That’s where the lines of code run. The other contexts “wait in line” to come back.
#Single-threaded - one thread, one stack
JavaScript has one thread of execution. One JS app = one call stack. Line by line, synchronously, one step at a time.
That’s why JS is called blocking. If a function takes 5 seconds (a heavy loop, parsing a giant JSON), the whole app stops for those 5 seconds. The thread can’t do anything else - any other callbacks, clicks, requests have to wait until the current function finishes.
The exception is web workers. Each worker gets:
- its own thread of execution,
- its own call stack,
- its own memory.
A worker lives in isolation - it can’t see the main thread’s variables and has no access to the DOM. It talks to the main thread through messages (postMessage / onmessage). Two parallel worlds talking to each other through a channel like two separate programs.
In practice: for most JS code, this doesn’t matter. You’re on one thread, one stack, and you write as if the world were synchronous.
#What follows from this
This simple mechanism - execution context plus call stack plus a single thread - is the foundation of three seemingly different things:
-
Closure. After
return, a local context is normally destroyed. But if a function returned another function that “holds on to” variables from its parent - those variables don’t disappear. The engine keeps them alive because someone still needs them. That’s a closure. -
this. Every execution context has its ownthis. Its value depends on how the function was called (not where it was defined) - four binding rules (default / implicit / explicit / new) plus arrow functions, which break out of the system and drawthislexically from their surroundings. -
Async. If the thread is single and synchronous, where do
setTimeout,fetch, and promises actually “happen”? The answer is the event loop: callbacks wait off the stack (in special queues - microtask or task queue) and only get put on the stack when the stack is empty. Anything that looks “parallel” in JS is, in reality, sequential - the thread just interleaves tasks.
These three look like separate topics, but they’re really just different ways of using the same engine.
#Takeaway
It boils down to two ideas:
- Execution context - a container for what’s currently running (thread + memory). One global, plus one local for every active function.
- Call stack - a list of those containers, ordered from oldest (the global, at the bottom) to the one running right now (on top). Every function call is a push, every
returnis a pop.
Every line of JS you write runs in some context. Every function call creates a new one. Every return closes that context. The stack remembers the way back.
The rest - closure, this, async - are consequences of this simple mechanism.
Comments
Loading comments…