Execution Contexts And The Call Stack
What an execution context actually contains, why arrow functions quietly skip two of its pieces, and how the call stack keeps track of which function gets to resume when another one finishes.
The last post mentioned the call stack in one line: execution contexts get pushed and popped in an order that has to make sense. That line is worth unpacking properly, because the call stack is the actual mechanism that keeps a JavaScript program from losing its place.
Top-Level Code Runs First, Always
The moment your code finishes compiling, the engine creates a global execution context for whatever code sits outside of any function. Function bodies do not run yet, even if they are declared right there in the file, since a function only runs once something actually calls it.
const storeName = 'Nimbus Goods';
function calculateSubtotal(price, qty) {
return price * qty;
}
const total = calculateSubtotal(25, 3);storeName and total are top-level code, so they execute immediately. calculateSubtotal only gets declared at this point, and its body waits for the call on the last line before it does anything.
What an Execution Context Actually Is
An execution context is an abstract idea, but it helps to picture it as a container holding everything one piece of code needs to run: its local variables, whatever arguments got passed in, and a few other things covered below.
Think about moving into a new apartment with a truck full of labeled boxes. Each box holds exactly what one room needs, nothing from the kitchen box ends up mixed into the bedroom box. An execution context works the same way: it is a sealed container of exactly what one function call needs, kept apart from every other function's container.
There is always exactly one global execution context in any JavaScript program, no matter how big it gets. It is the default container, and it is where all of your top-level code lives.
Every function call gets this same treatment, one new context per call, and that includes methods too, since a method is just a function that happens to be attached to an object.
What's Inside a Context
Every execution context is built from the same three things, generated in something called the creation phase, right before that code actually starts running.
- The variable environment: every variable and function declaration that belongs to this context, plus a special
argumentsobject listing whatever was passed in when the function was called. - The scope chain: references to variables that live outside the current function but are still reachable from it. This series has already used that reachability constantly without naming it, and it gets its own full explanation soon.
- The
thiskeyword: a special variable every context gets, also getting its own dedicated post later in this chapter.
One detail matters more than the rest right now: arrow functions do not get their own arguments object, and they do not get their own this. Instead, they borrow both from whichever regular function encloses them.
function regularOuter() {
const arrowInner = () => {
console.log(arguments); // borrowed from regularOuter, not its own
};
arrowInner();
}
regularOuter(1, 2, 3); // logs [1, 2, 3]Hold onto that fact. It comes back with real weight once this gets its own post, since it's the single biggest practical difference between the two function styles.
Simulating the Creation Phase
Take this small example and walk through what each context's variable environment looks like the instant it's created, before a single line has actually run:
const storeName = 'Nimbus Goods';
function calculateSubtotal(price, qty) {
const lineTotal = price * qty;
return applyDiscount(lineTotal);
}
function applyDiscount(amount) {
return amount * 0.9;
}
const total = calculateSubtotal(25, 3);The global context's variable environment holds storeName, both function declarations, and total, but total's actual value is still unknown at creation time. It only gets resolved once calculateSubtotal finishes running.
calculateSubtotal's own context, created only once it's called, holds lineTotal (also unresolved at creation) and an arguments object containing [25, 3]. applyDiscount's context, created only once it gets called from inside calculateSubtotal, holds amount and an arguments object containing whatever lineTotal resolved to.
None of these values are actually known during creation. They only become real numbers once execution reaches that line, the creation phase just sets up the empty container each value will eventually land in.
The Call Stack: Where Contexts Actually Stack Up
Three functions is manageable to track by hand. A real application might be a hundred functions deep at any given moment, and something has to keep track of exactly where execution needs to return to once each one finishes.
That something is the call stack, and it works exactly like the moving truck from before: boxes get stacked on top of each other, and whichever box is on top is the one currently being unpacked.
Walk through the example above step by step:
- The global execution context gets created and pushed onto the stack. Top-level code starts running inside it.
- Execution reaches
calculateSubtotal(25, 3). A new context gets created for that call and pushed on top. The global context is still there, just paused underneath. - Inside
calculateSubtotal, execution reachesapplyDiscount(lineTotal). A third context gets pushed on top of that.calculateSubtotalis now paused too, waiting. applyDiscountfinishes and returns. Its context gets popped off the stack entirely, andcalculateSubtotalresumes exactly where it left off, now with the returned value in hand.calculateSubtotalfinishes and returns. Its context also gets popped, and the global context resumes, assigning the final result tototal.
Without the stack keeping track of that order, the engine would have no way of knowing which paused function to return to. It is not a suggestion or a convenience, it is the literal mechanism that makes nested function calls work at all.
ExpandThe call stack at its deepest point in the example: global context at the bottom, calculateSubtotal paused above it, applyDiscount active on top, then popping back down in order as each function returns.
Only one context is ever active at a time, since JavaScript runs on a single thread. calculateSubtotal genuinely stops making progress while applyDiscount runs, and only continues once applyDiscount's context is gone. This series already covered why that single-threaded constraint doesn't mean JavaScript freezes on slow work, back in the event loop post, the call stack is the piece that makes that whole story make sense.
The global execution context is the one exception to all this popping. It stays on the stack for the entire life of the program, only disappearing once the page (or the Node process) actually closes.
The Essentials
- A global execution context is created first, for top-level code only. Function bodies stay dormant until something actually calls them.
- An execution context is a container for one piece of code's local state: its variable environment, its scope chain, and its
thisvalue, generated during a creation phase before execution starts. - Arrow functions never get their own
argumentsobject orthis. Both are borrowed from the nearest enclosing regular function instead. - The call stack tracks which context is active by stacking them. A new function call pushes a context on top; a
returnpops it off and resumes whatever was paused underneath. - Only one context runs at a time. A paused function makes zero progress until every context stacked above it finishes and gets popped.
- The global execution context is the only one that survives for the entire program, disappearing only when the program itself ends.
That arguments object mentioned above lives inside something called the variable environment, and the next post picks up exactly there: what actually gets stored, and how hoisting quietly decides what's available before a single line runs.
Keep reading