Skip to main content
How does JavaScript keep track of which function is running? When a function calls another function, how does JavaScript know where to return when that function finishes? The answer is the call stack. It’s JavaScript’s mechanism for tracking function execution.
function greet(name) {
  const message = createMessage(name)
  console.log(message)
}

function createMessage(name) {
  return "Hello, " + name + "!"
}

greet("Alice")  // "Hello, Alice!"
When greet calls createMessage, JavaScript remembers where it was in greet so it can return there after createMessage finishes. The call stack is what makes this possible.
What you’ll learn in this guide:
  • What the call stack is and why JavaScript needs it
  • How functions are added and removed from the stack
  • What happens step-by-step when your code runs
  • Why you sometimes see “Maximum call stack size exceeded” errors
  • How to debug call stack issues like a pro
Prerequisite: This guide assumes basic familiarity with JavaScript functions. If you’re new to functions, start there first!

The Stack of Plates: A Real-World Analogy

Imagine you’re working in a restaurant kitchen, washing dishes. As clean plates come out, you stack them one on top of another. When a server needs a plate, they always take the one from the top of the stack, not from the middle or bottom.
        ┌───────────┐
        │  Plate 3  │  ← You add here (top)
        ├───────────┤
        │  Plate 2  │
        ├───────────┤
        │  Plate 1  │  ← First plate (bottom)
        └───────────┘
This is exactly how JavaScript keeps track of your functions! When you call a function, JavaScript puts it on top of a “stack.” When that function finishes, JavaScript removes it from the top and goes back to whatever was underneath. This simple concept, adding to the top and removing from the top, is the foundation of how JavaScript executes your code.

What is the Call Stack?

The call stack is a mechanism that JavaScript uses to keep track of where it is in your code. Think of it as JavaScript’s “to-do list” for function calls, but one where it can only work on the item at the top.
function first() { second(); }
function second() { third(); }
function third() { console.log('Hello!'); }

first();
// Stack grows: [first] → [second, first] → [third, second, first]
// Stack shrinks: [second, first] → [first] → []

The LIFO Principle

The call stack follows a principle called LIFO: Last In, First Out.
  • Last In: The most recent function call goes on top
  • First Out: The function on top must finish before we can get to the ones below
LIFO = Last In, First Out

┌─────────────────┐
│   function C    │  ← Last in (most recent call)
├─────────────────┤     First to finish and leave
│   function B    │
├─────────────────┤
│   function A    │  ← First in (earliest call)
└─────────────────┘     Last to finish

Why Does JavaScript Need a Call Stack?

JavaScript is single-threaded, meaning it can only do one thing at a time. According to the ECMAScript specification, each function invocation creates a new execution context that gets pushed onto the stack. The call stack helps JavaScript:
  1. Remember where it is — Which function is currently running?
  2. Know where to go back — When a function finishes, where should execution continue?
  3. Keep track of local variables — Each function has its own variables that shouldn’t interfere with others
ECMAScript Specification: According to the official JavaScript specification, the call stack is implemented through “execution contexts.” Each function call creates a new execution context that gets pushed onto the stack.

How the Call Stack Works: Step-by-Step

Let’s trace through a simple example to see the call stack in action.

A Simple Example

function greet(name) {
  const greeting = createGreeting(name);
  console.log(greeting);
}

function createGreeting(name) {
  return "Hello, " + name + "!";
}

// Start here
greet("Alice");
console.log("Done!");

Step-by-Step Execution

1

Program Starts

JavaScript begins executing your code from top to bottom. The call stack is empty.
Call Stack: [ empty ]
2

greet('Alice') is Called

JavaScript sees greet("Alice") and pushes greet onto the call stack.
Call Stack: [ greet ]
Now JavaScript enters the greet function and starts executing its code.
3

createGreeting('Alice') is Called

Inside greet, JavaScript encounters createGreeting(name). It pushes createGreeting onto the stack.
Call Stack: [ createGreeting, greet ]
Notice: greet is paused while createGreeting runs. JavaScript can only do one thing at a time!
4

createGreeting Returns

createGreeting finishes and returns "Hello, Alice!". JavaScript pops it off the stack.
Call Stack: [ greet ]
The return value ("Hello, Alice!") is passed back to greet.
5

greet Continues and Finishes

Back in greet, the returned value is stored in greeting, then console.log runs. Finally, greet finishes and is popped off.
Call Stack: [ empty ]
6

Program Continues

With the stack empty, JavaScript continues to the next line: console.log("Done!").Output:
Hello, Alice!
Done!

Visual Summary

Step 1:          Step 2:          Step 3:              Step 4:          Step 5:

┌─────────┐      ┌─────────┐      ┌────────────────┐   ┌─────────┐      ┌─────────┐
│ (empty) │  →   │  greet  │  →   │createGreeting  │ → │  greet  │  →   │ (empty) │
└─────────┘      └─────────┘      ├────────────────┤   └─────────┘      └─────────┘
                                  │     greet      │
                                  └────────────────┘

Program          greet()          createGreeting()     createGreeting   greet()
starts           called           called               returns          returns

Execution Context: What’s Actually on the Stack?

When we say a function is “on the stack,” what does that actually mean? Each entry on the call stack is called an execution context, sometimes referred to as a stack frame in general computer science terms. It contains everything JavaScript needs to execute that function.
The values passed to the function when it was called.
function greet(name, age) {
  // Arguments: { name: "Alice", age: 25 }
}
greet("Alice", 25);
Variables declared inside the function with var, let, or const.
function calculate() {
  const x = 10;      // Local variable
  let y = 20;        // Local variable
  var z = 30;        // Local variable
  // These only exist inside this function
}
The value of this inside the function, which depends on how the function was called.
const person = {
  name: "Alice",
  greet() {
    console.log(this.name); // 'this' refers to person
  }
};
Where JavaScript should continue executing after this function returns. This is how JavaScript knows to go back to the right place in your code.
Access to variables from outer (parent) functions. This is how closures work!
function outer() {
  const message = "Hello";
  
  function inner() {
    console.log(message); // Can access 'message' from outer
  }
  
  inner();
}

Visualizing an Execution Context

┌─────────────────────────────────────────┐
│         EXECUTION CONTEXT               │
│         Function: greet                 │
├─────────────────────────────────────────┤
│  Arguments:    { name: "Alice" }        │
│  Local Vars:   { greeting: undefined }  │
│  this:         window (or undefined)    │
│  Return to:    line 12, main program    │
│  Outer Scope:  [global scope]           │
└─────────────────────────────────────────┘

Nested Function Calls: A Deeper Example

Let’s look at a more complex example with multiple levels of function calls.
function multiply(x, y) {
  return x * y;
}

function square(n) {
  return multiply(n, n);
}

function printSquare(n) {
  const result = square(n);
  console.log(result);
}

printSquare(4);

Tracing the Execution

Step 1: Call printSquare(4)
Stack: [ printSquare ]
Step 2: printSquare calls square(4)
Stack: [ square, printSquare ]
Step 3: square calls multiply(4, 4)
Stack: [ multiply, square, printSquare ]
This is the maximum stack depth for this program: 3 frames.Step 4: multiply returns 16
Stack: [ square, printSquare ]
Step 5: square returns 16
Stack: [ printSquare ]
Step 6: printSquare logs 16 and returns
Stack: [ empty ]
Output: 16
Understanding the flow: Each function must completely finish before the function that called it can continue. This is why printSquare has to wait for square, and square has to wait for multiply.

The #1 Call Stack Mistake: Stack Overflow

The call stack has a limited size. The default limit varies by engine — Chrome’s V8 typically allows around 10,000–15,000 frames, while Firefox’s SpiderMonkey has a similar threshold. If you keep adding functions without removing them, eventually you’ll run out of space. This is called a stack overflow, and JavaScript throws a RangeError when it happens.
┌─────────────────────────────────────────────────────────────────────────┐
│                         STACK OVERFLOW                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  WRONG: No Base Case                    RIGHT: With Base Case            │
│  ────────────────────                   ─────────────────────            │
│                                                                          │
│  function count() {                     function count(n) {              │
│    count()  // Forever!                   if (n <= 0) return  // Stop!   │
│  }                                        count(n - 1)                   │
│                                         }                                │
│                                                                          │
│  Stack grows forever...                 Stack grows, then shrinks        │
│  ┌─────────┐                            ┌─────────┐                      │
│  │ count() │                            │ count(0)│ ← Returns            │
│  ├─────────┤                            ├─────────┤                      │
│  │ count() │                            │ count(1)│                      │
│  ├─────────┤                            ├─────────┤                      │
│  │ count() │                            │ count(2)│                      │
│  ├─────────┤                            └─────────┘                      │
│  │  ....   │                                                             │
│  └─────────┘                                                             │
│  💥 CRASH!                              ✓ Success!                       │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
The Trap: Recursive functions without a proper stopping condition will crash your program. The most common cause is infinite recursion, a function that calls itself forever without a base case.

The Classic Mistake: Missing Base Case

// ❌ BAD: This will crash!
function countdown(n) {
  console.log(n);
  countdown(n - 1);  // Calls itself forever!
}

countdown(5);
What happens:
Stack: [ countdown(5) ]
Stack: [ countdown(4), countdown(5) ]
Stack: [ countdown(3), countdown(4), countdown(5) ]
Stack: [ countdown(2), countdown(3), countdown(4), countdown(5) ]
... keeps growing forever ...
💥 CRASH: Maximum call stack size exceeded

The Fix: Add a Base Case

// ✅ GOOD: This works correctly
function countdown(n) {
  if (n <= 0) {
    console.log("Done!");
    return;  // ← BASE CASE: Stop here!
  }
  console.log(n);
  countdown(n - 1);
}

countdown(5);
// Output: 5, 4, 3, 2, 1, Done!
What happens now:
Stack: [ countdown(5) ]
Stack: [ countdown(4), countdown(5) ]
Stack: [ countdown(3), countdown(4), countdown(5) ]
Stack: [ countdown(2), countdown(3), ..., countdown(5) ]
Stack: [ countdown(1), countdown(2), ..., countdown(5) ]
Stack: [ countdown(0), countdown(1), ..., countdown(5) ]
       ↑ Base case reached! Start returning.
Stack: [ countdown(1), ..., countdown(5) ]
Stack: [ countdown(2), ..., countdown(5) ]
... stack unwinds ...
Stack: [ countdown(5) ]
Stack: [ empty ]
✅ Program completes successfully

Error Messages by Browser

BrowserError Message
ChromeRangeError: Maximum call stack size exceeded
FirefoxInternalError: too much recursion (non-standard)
SafariRangeError: Maximum call stack size exceeded
Firefox uses InternalError which is a non-standard error type specific to the SpiderMonkey engine. Chrome and Safari use the standard RangeError.

Common Causes of Stack Overflow

// Missing the stopping condition
function loop() {
  loop();
}
loop(); // 💥 Crash!
function countUp(n) {
  if (n >= 1000000000000) return; // Too far away!
  countUp(n + 1);
}
countUp(0); // 💥 Crash before reaching base case
class Person {
  set name(value) {
    this.name = value; // Calls the setter again! Infinite loop!
  }
}

const p = new Person();
p.name = "Alice"; // 💥 Crash!

// Fix: Use a different property name
class PersonFixed {
  set name(value) {
    this._name = value; // Use _name instead
  }
}
function a() { b(); }
function b() { a(); } // a calls b, b calls a, forever!

a(); // 💥 Crash!
Prevention tips:
  1. Always define a clear base case for recursive functions
  2. Make sure each recursive call moves toward the base case
  3. Consider using iteration (loops) instead of recursion for simple cases
  4. Be careful with property setters, use different internal property names

Debugging the Call Stack

When something goes wrong, the call stack is your best friend for figuring out what happened.

Reading a Stack Trace

When an error occurs, JavaScript gives you a stack trace, a snapshot of the call stack at the moment of the error.
function a() { b(); }
function b() { c(); }
function c() { 
  throw new Error('Something went wrong!'); 
}

a();
Output:
Error: Something went wrong!
    at c (script.js:4:9)
    at b (script.js:2:14)
    at a (script.js:1:14)
    at script.js:7:1
How to read it:
  • Read from top to bottom = most recent call to oldest
  • at c (script.js:4:9) = Error occurred in function c, file script.js, line 4, column 9
  • The trace shows you exactly how the program got to the error

Using Browser DevTools

1

Open DevTools

Press F12 or Cmd+Option+I (Mac) / Ctrl+Shift+I (Windows)
2

Go to Sources Tab

Click on the “Sources” tab (Chrome) or “Debugger” tab (Firefox)
3

Set a Breakpoint

Click on a line number in your code to set a breakpoint. Execution will pause there.
4

View the Call Stack

When paused, look at the “Call Stack” panel on the right. It shows all the functions currently on the stack.
5

Step Through Code

Use the step buttons to execute one line at a time and watch the stack change.
Pro debugging tip: If you’re dealing with recursion, add a console.log at the start of your function to see how many times it’s being called:
function factorial(n) {
  console.log('factorial called with n =', n);
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

The Call Stack and Asynchronous Code

You might be wondering: “If JavaScript can only do one thing at a time, how does it handle things like setTimeout or fetching data from a server?” Great question! The call stack is only part of the picture.
When you use asynchronous functions like setTimeout, fetch, or event listeners, JavaScript doesn’t put them on the call stack immediately. Instead, they go through a different system involving the Event Loop and Task Queue.This is covered in detail in the Event Loop section.
Here’s a sneak peek:
console.log('First');

setTimeout(() => {
  console.log('Second');
}, 0);

console.log('Third');

// Output:
// First
// Third
// Second  ← Even with 0ms delay, this runs last!
The setTimeout callback doesn’t go directly on the call stack. It waits in a queue until the stack is empty. As Philip Roberts demonstrated in his acclaimed JSConf EU talk “What the heck is the event loop?” (viewed over 8 million times), this is why “Third” prints before “Second” even though the timeout is 0 milliseconds.

Common Misconceptions

Wrong! The call stack and heap are completely different structures:
ComponentPurposeStructure
Call StackTracks function executionOrdered (LIFO), small, fast
HeapStores data (objects, arrays)Unstructured, large
function example() {
  // Primitives live in the stack frame
  const x = 10;
  const name = "Alice";
  
  // Objects live in the HEAP (reference stored in stack)
  const user = { name: "Alice" };
  const numbers = [1, 2, 3];
}
When the function returns, the stack frame is popped (primitives gone), but heap objects persist until garbage collected.
Wrong! When a timer finishes, the callback does NOT run immediately. It goes to the Task Queue and must wait for:
  1. The call stack to be completely empty
  2. All microtasks to be processed first
  3. Its turn in the task queue
console.log('Start');

setTimeout(() => {
  console.log('Timer');  // Does NOT run at 0ms!
}, 0);

console.log('End');

// Output: Start, End, Timer
// Even with 0ms delay, 'Timer' prints LAST
The callback must wait until the current script finishes and the stack is empty.
Wrong! JavaScript is single-threaded. It has ONE call stack and can only execute ONE thing at a time.
function a() { 
  console.log('A start');
  b();  // JS pauses 'a' and runs 'b' completely
  console.log('A end');
}

function b() { 
  console.log('B');
}

a();
// Output: A start, B, A end (sequential, not parallel)
The source of confusion: People mistake JavaScript’s asynchronous behavior for parallel execution. Web APIs (timers, fetch, etc.) run in separate browser threads, but JavaScript code itself runs one operation at a time. The Event Loop coordinates callbacks, creating the illusion of concurrency.
Wrong! The Promise constructor runs synchronously. Only the .then() callbacks are asynchronous:
console.log('1');

new Promise((resolve) => {
  console.log('2');  // Runs SYNCHRONOUSLY!
  resolve();
}).then(() => {
  console.log('3');  // Async (microtask)
});

console.log('4');

// Output: 1, 2, 4, 3
// Note: '2' prints before '4'!
The executor function passed to new Promise() runs immediately on the call stack. Only the .then(), .catch(), and .finally() callbacks are queued as microtasks.

Key Takeaways

The key things to remember about the Call Stack:
  1. JavaScript is single-threaded — It has ONE call stack and can only do one thing at a time
  2. LIFO principle — Last In, First Out. The most recent function call finishes first
  3. Execution contexts — Each function call creates a “frame” containing arguments, local variables, and return address
  4. Synchronous execution — Functions must complete before their callers can continue
  5. Stack overflow — Happens when the stack gets too deep, usually from infinite recursion
  6. Always have a base case — Recursive functions need a stopping condition
  7. Stack traces are your friend — They show you exactly how your program got to an error
  8. Async callbacks waitsetTimeout, fetch, and event callbacks don’t run until the call stack is empty
  9. Each frame is isolated — Local variables in one function call don’t affect variables in another call of the same function
  10. Debugging tools show the stack — Browser DevTools let you pause execution and inspect the current call stack

Test Your Knowledge

Answer: LIFO stands for Last In, First Out.It’s important because it determines the order in which functions execute and return. The most recently called function must complete before the function that called it can continue. This is how JavaScript keeps track of nested function calls and knows where to return when a function finishes.
function a() { b(); }
function b() { c(); }
function c() { d(); }
function d() { console.log('done'); }
a();
Answer: The maximum stack depth is 4 frames.
Stack at deepest point: [ d, c, b, a ]
When d() is executing, all four functions are on the stack. After d() logs “done” and returns, the stack starts unwinding.
function greet() {
  greet();
}
greet();
Answer: This code causes a stack overflow because there’s no base case to stop the recursion.
  • greet() is called
  • greet() calls greet() again
  • That greet() calls greet() again
  • This continues forever, adding new frames to the stack
  • Eventually the stack runs out of space → Maximum call stack size exceeded
Fix: Add a condition to stop the recursion:
function greet(times) {
  if (times <= 0) return;  // Base case
  console.log('Hello!');
  greet(times - 1);
}
greet(3);
Answer: An execution context (stack frame) contains:
  1. Function arguments — The values passed to the function
  2. Local variables — Variables declared with var, let, or const
  3. The this value — The context binding for the function
  4. Return address — Where to continue executing after the function returns
  5. Scope chain — Access to variables from outer (parent) functions
This is why each function call can have its own independent set of variables without interfering with other calls.
console.log('First')

setTimeout(() => {
  console.log('Second')
}, 0)

console.log('Third')
Answer: The output is:
First
Third
Second
Even though setTimeout has a 0ms delay, “Second” prints last because:
  1. setTimeout doesn’t put the callback directly on the call stack
  2. Instead, the callback waits in the task queue
  3. The event loop only moves it to the call stack when the stack is empty
  4. “Third” runs first because it’s already on the call stack
This demonstrates that the call stack must be empty before async callbacks execute.
Given this error:
Error: Something went wrong!
    at c (script.js:4:9)
    at b (script.js:2:14)
    at a (script.js:1:14)
    at script.js:7:1
Answer: Read stack traces from top to bottom (most recent to oldest):
  1. Top line (at c) — Where the error actually occurred (function c, line 4, column 9)
  2. Following lines — The chain of function calls that led here
  3. Bottom line — Where the chain started (the initial call)
The trace tells you: the program started at line 7, called a(), which called b(), which called c(), where the error was thrown. This helps you trace back through your code to find the root cause.

Frequently Asked Questions

The call stack is a LIFO (Last In, First Out) data structure that JavaScript uses to track function execution. According to the ECMAScript specification, each function call creates an “execution context” that is pushed onto the stack. When a function returns, its context is popped off and execution resumes in the calling function.
A stack overflow occurs when the call stack exceeds its maximum size, typically around 10,000–15,000 frames in V8 (Chrome, Node.js). The most common cause is infinite recursion — a function that calls itself without a proper base case. JavaScript throws a RangeError: Maximum call stack size exceeded when this happens.
JavaScript was designed as a single-threaded language to simplify DOM manipulation — having multiple threads modifying the page simultaneously would create race conditions. As documented in MDN, JavaScript has one call stack and can only execute one piece of code at a time. Asynchronous behavior is achieved through the event loop, Web APIs, and task queues rather than multiple threads.
An execution context is the environment in which JavaScript code is evaluated and executed. It contains the function’s arguments, local variables, the this binding, a reference to the outer scope (scope chain), and the return address. The ECMAScript specification defines two types: the Global Execution Context (created when your script starts) and Function Execution Contexts (created on each function call).
The call stack handles synchronous code execution, while the event loop manages asynchronous callbacks. When an async operation (like setTimeout or fetch) completes, its callback is placed in a task queue. The event loop checks if the call stack is empty, and only then moves the next callback onto the stack for execution. This architecture was popularized by Philip Roberts’ JSConf EU talk, which has been viewed over 8 million times.


Reference

Articles

Courses

Introduction to Asynchronous JavaScript — Piccalilli

Part of the “JavaScript for Everyone” course by Mat Marquis. This free lesson explains why JavaScript is single-threaded, how the call stack manages execution contexts, and introduces the event loop and concurrency model. Beautifully written with a fun narrative style.

Videos

What the heck is the event loop anyway?

🏆 The legendary JSConf talk that made mass developers finally “get” the event loop. Amazing visualizations — a must watch!

The JS Call Stack Explained In 9 Minutes

Short, sweet, and beginner-friendly. Colt Steele breaks down the call stack with practical examples.

How JavaScript Code is executed? & Call Stack

Part of the popular “Namaste JavaScript” series. Akshay Saini explains execution with great visuals and examples.

Understanding JavaScript Execution

Shows how JavaScript creates execution contexts and manages memory during function calls. Part of Codesmith’s excellent “JavaScript: The Hard Parts” series.

Javascript: the Call Stack explained

Traces through nested function calls line by line, showing exactly when frames are pushed and popped. Good for visual learners who want to see each step.

What is the Call Stack?

Uses a simple factorial example to demonstrate recursion on the call stack. Under 10 minutes, perfect for a quick refresher.

The Call Stack

Draws out the stack visually as code executes, making the LIFO concept easy to grasp. Includes a stack overflow example that shows what happens when things go wrong.

Call Stacks - CS50

Harvard’s CS50 explains call stacks from a computer science perspective — great for understanding the theory.

Learn the JavaScript Call Stack

Live codes examples while explaining each concept, so you see exactly how to trace execution yourself. Great for following along in your own editor.

JavaScript Functions and the Call Stack

Focuses on the relationship between function invocation and stack frames. Explains why understanding the call stack helps you debug errors faster.
Last modified on February 17, 2026