The call stack is how JavaScript keeps track of where it is. Every function call pushes a new frame on top, every return pops one off.
Complete lesson & earn 250 PX
EXERCISE
1The call stack is how JavaScript keeps track of where it is in your code. Think of it as a stack of plates: the one on top is the one being handled right now.
EXERCISE
2Every function call pushes a new context onto the stack. Every return pops one off. The program ends when the stack is empty.
EXERCISE
3When a function calls itself forever without stopping, the call stack fills up and crashes. Understanding why helps you write safer code.
A function call creates a new execution context, and that context has to go somewhere. When a function calls another function, which calls a third, the engine needs to remember the whole trail so it can return to the right place each time. The structure that holds it is the call stack.
It behaves the way the name suggests. A call pushes a context onto the top. A return pops it off. Only the context on top is running; everything below it is paused, waiting for the thing above to finish. The context at the very bottom is the global one, pushed when the program starts and popped when it ends.
Three functions, and a return path the engine has to remember:
function first() {
console.log("first");
second();
console.log("back in first");
}
function second() {
console.log("second");
}
first();
The output is first, second, back in first. That third line is the part worth paying to. When second returns, execution does not carry on from wherever it likes. It resumes inside first, on the line after the call, because first was sitting on the stack the whole time with its place kept.
This ordering is strict, and the term for it is last in, first out. The most recently called function is always the first to finish:
function outer() {
console.log("outer start");
middle();
console.log("outer end");
}
function middle() {
console.log("middle start");
console.log("middle end");
}
outer();
Reading the output top to bottom gives outer start, middle start, middle end, outer end. The calls nest, so the logs nest with them. outer cannot print its final line until middle has completely finished and been removed from the stack, no matter how long that takes.
You have already been reading this structure without knowing it. When an error prints a stack in your console, those indented lines are the call stack at the moment things went wrong, listed from the innermost call outward. The names you see are the functions that were paused, in the order they were waiting.
The stack is easiest to trust once you have watched it grow to its deepest point and unwind all the way back. Depth here means nesting: how many calls are open at once, not how many lines have run.
Three levels, with logs on both sides of each call:
function outer() {
console.log("outer start");
middle();
console.log("outer end");
}
function middle() {
console.log("middle start");
inner();
console.log("middle end");
}
function inner() {
console.log("inner");
}
outer();
At the deepest moment, four contexts are open at once: the global one, then outer, then middle, then inner. Only inner is running. The other three are frozen mid-statement, each waiting on the call it made.
Then it unwinds. inner returns and is discarded, and middle picks up exactly where it stopped. middle returns, and outer resumes. The output reads outer start, middle start, inner, middle end, outer end, which is the stack growing and shrinking written out as text.
The engine does not keep this up forever. Each open context costs memory, so there is a ceiling:
function depth(n) {
return depth(n + 1);
}
depth(1);
// RangeError: Maximum call stack size exceeded
Nothing here is wrong in the sense of a typo. depth is valid that calls itself correctly. It simply never stops, so contexts pile up until the engine refuses to allocate another and throws.
The limit is somewhere in the thousands to tens of thousands of frames, and it varies by engine and by how much each frame holds. It is deliberately not a number worth memorising. What is worth taking away is that recursion depth is a real, finite resource, and that a RangeError with this message almost always means a missing or unreachable stopping condition rather than genuinely deep work.
Pro Tip:
RangeError: Maximum call stack size exceededand an out-of-memory error are different failures. The stack overflow is about too many open calls; running out of heap is about too much data. Reaching for the wrong one sends you looking in the wrong place.
A function that calls itself is recursion, and it is a completely ordinary thing to do. What makes it safe is not avoiding it but making sure every path through the function eventually reaches a case that returns without calling again. That case is the base case, and leaving it out is the single most common way to overflow the stack.
A recursion that terminates, and the reason it does:
function countdown(n) {
if (n <= 0) {
console.log("Done");
return;
}
console.log(n);
countdown(n - 1);
}
countdown(3);
This prints 3, 2, 1, Done. Four contexts open at the deepest point, then all four unwind. The base case is n <= 0, and every call moves towards it because n shrinks by one each time. Both halves matter: a base case that the argument never reaches is no better than not having one.
The same function with the base case removed:
function countdown(n) {
console.log(n);
countdown(n - 1);
}
countdown(3);
// 3, 2, 1, 0, -1, -2 ... RangeError
It counts past zero into negative numbers and keeps going until the stack is full. Notice that this version is not obviously broken by sight. It is one missing if away from the correct one, which is exactly why the error message is worth recognising immediately.
A subtler version of the same mistake has a base case that is simply unreachable:
function halve(n) {
if (n === 0) {
return "done";
}
return halve(n / 2);
}
halve(8);
// RangeError: Maximum call stack size exceeded
Dividing by two never produces exactly 0, only smaller and smaller fractions, so n === 0 is never true. Changing the condition to n < 1 fixes it. The lesson generalises: when a recursive function overflows, check whether the base case exists and whether the values you are generating can actually arrive at it.
Pro Tip: Read stack traces from the top down. The innermost call is listed first, and the repeated frame in an overflow tells you which function failed to terminate, usually in one glance.
The call stack follows Last In, First Out. The Global Execution Context sits at the bottom. Functions push on top when called and pop off when they return.