Skip to main content

What is the call stack in JavaScript?

Call stack is the data structure JavaScript uses to track which function is currently running and where to return when it finishes.

Theory

TL;DR

  • The call stack follows LIFO (Last In, First Out): the last function pushed is the first to pop
  • Each function call creates a stack frame holding local variables, the this binding, and a return address
  • JavaScript has one call stack, so only one thing executes at a time
  • When the stack is empty, the event loop can push the next task
  • Infinite recursion without a base case fills the stack and throws RangeError: Maximum call stack size exceeded

How the stack works

When JavaScript calls a function, it pushes a frame onto the stack. When that function returns, the frame is popped off. The engine always executes whatever is on top.

javascript
function greet(name) { return `Hello, ${name}`; } function run() { const message = greet('Alice'); // greet pushed, then popped console.log(message); // run still on stack } run(); // Stack: [run] -> [run, greet] -> [run] -> []

After run() is called, the stack is [run]. When greet is invoked inside, it becomes [run, greet]. When greet returns, back to [run]. When run finishes, the stack empties.

Stack frames

Each frame is not just a pointer. It holds the function's local variables, the value of this, and the return address. This is why deep recursion eats memory: every frame takes space, and none are freed until the function returns.

Stack overflow

No base case in a recursive function means frames pile up forever.

javascript
function countdown(n) { console.log(n); countdown(n - 1); // never stops } countdown(10000); // RangeError: Maximum call stack size exceeded

V8 (Chrome, Node.js) typically throws around 10,000 to 12,000 frames deep. The fix is a base case:

javascript
function countdown(n) { if (n < 0) return; // base case console.log(n); countdown(n - 1); }

Single-threaded execution

JavaScript has one call stack. That is a design choice, not a constraint. It makes code predictable: no race conditions, no shared mutable state, no locks needed.

But it means one heavy operation blocks everything. A while loop running for 5 seconds freezes the tab because the stack never empties and no other code can run. CPU-heavy work belongs in a Web Worker.

Call stack and the event loop

The event loop watches the call stack. When it is empty, the loop picks the next callback from the task queue and pushes it onto the stack. Async callbacks, timers, and Promise handlers all wait in the queue until the stack clears.

javascript
console.log('start'); setTimeout(() => { console.log('timeout'); // queued, runs after stack clears }, 0); console.log('end'); // Output: // start // end // timeout

Even with 0ms delay, setTimeout runs after end. The callback waits in the queue while console.log('end') is still on the stack.

Reading error stack traces

When an error is thrown, the stack trace shows the call stack at that exact moment. Read it bottom-up for call order.

javascript
function a() { b(); } function b() { c(); } function c() { throw new Error('oops'); } a(); // Error: oops // at c (file.js:3) // at b (file.js:2) // at a (file.js:1)

Bottom of the trace (a) is where execution started. Top (c) is where it crashed. I find this trips up developers who read only the first line and stop there.

Common mistakes

1. Thinking async callbacks run on the current stack

javascript
function fetchData() { fetch('/api/data').then(res => { console.log('inside then'); }); console.log('after fetch'); } fetchData(); // Output: // after fetch // inside then <- runs AFTER fetchData has already left the stack

The .then callback runs after fetchData popped off. It was waiting in the microtask queue.

2. Assuming setTimeout(fn, 0) fires immediately

Zero milliseconds means "after the current stack is empty," not "right now." If the stack stays busy for 2 seconds, the callback waits those 2 seconds plus the 0ms delay.

3. Blocking the stack with synchronous loops

javascript
const start = Date.now(); while (Date.now() - start < 3000) {} // busy-wait for 3 seconds console.log('done'); // No event, click, or timer fired during those 3 seconds

Move expensive work to a Web Worker or break it into chunks with setTimeout.

4. Misreading stack traces

Stack traces read bottom-to-top by call order, but the error is at the top. Read the whole trace to find where the call originated.

Real-world usage

  • Browser DevTools: the "Call Stack" panel shows the live stack as you step through code
  • React: component render functions appear on the stack during reconciliation; render errors show a component stack trace
  • Express: middleware chains are nested calls; unhandled errors travel up the stack to the error handler
  • Node.js: Error.captureStackTrace captures the call stack at any point for custom diagnostics

Follow-up questions

Q: What happens when the call stack is empty?
A: The event loop checks the microtask queue first (Promise callbacks), drains it completely, then takes one task from the macrotask queue (setTimeout, setInterval) and pushes it onto the stack.

Q: Why does a long for loop block UI in the browser?
A: The loop keeps the call stack occupied the entire time. The event loop cannot push any callbacks (clicks, repaints, timers) until the stack clears. Break the work into chunks using setTimeout or requestAnimationFrame.

Q: Does async/await interact with the call stack differently?
A: Code before the first await runs on the stack normally. At await, the function suspends and its frame is removed from the stack. When the awaited Promise resolves, the function resumes as a microtask and a new frame is pushed for the code after await.

Q: What exactly is a stack frame?
A: A block of memory the engine allocates for one function call. It holds local variables, the arguments object, the this binding, and a return address. The frame is freed when the function returns.

Q: Can you increase the maximum stack size?
A: In Node.js, yes: node --stack-size=65536 app.js. In browsers, no. The practical fix for deep recursion is to rewrite it iteratively or use trampolining.

Examples

Basic stack trace

javascript
function add(a, b) { return a + b; // stack: [add] } function calculate(x, y) { const result = add(x, y); // stack: [calculate, add] -> [calculate] return result; } function main() { const total = calculate(5, 3); // stack: [main, calculate] -> [main] -> [] console.log(total); // 8 } main();

main calls calculate, which calls add. Each call pushes a frame. Each return pops one. After main exits, the stack is empty and the output is 8.

Order processing with error tracking

javascript
function processOrder(order) { validateOrder(order); const total = calcTotal(order); notifyUser(order, total); } function validateOrder(order) { if (!order.items || order.items.length === 0) { throw new Error('Order has no items'); // Stack at this point: [validateOrder, processOrder] } } function calcTotal(order) { return order.items.reduce((sum, item) => sum + item.price, 0); } function notifyUser(order, total) { console.log(`Order ${order.id} confirmed. Total: $${total}`); } processOrder({ id: 42, items: [{ price: 19.99 }, { price: 5.50 }], email: 'user@example.com' }); // Order 42 confirmed. Total: $25.49

If validateOrder throws, the error bubbles up through processOrder. The stack trace shows both frames, making the bug easy to locate.

Async code and the stack

javascript
async function getUserData(userId) { // Stack: [getUserData] while this line starts const response = await fetch(`/api/users/${userId}`); // Stack is empty during fetch - getUserData is suspended // Stack: [getUserData] resumes here when the Promise resolves return response.json(); } async function renderProfile(userId) { const user = await getUserData(userId); console.log(`Rendering profile for ${user.name}`); } renderProfile(1); // Stack is not blocked during fetch; other code can run

The stack is not blocked during fetch. The function suspends, freeing the stack for other work. That is the whole point of async/await.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?