LoopLens

How the JavaScript call stack works

The call stack is how JavaScript keeps track of which function is running and where to go back to when it finishes. Calling a function puts a new frame on top of the stack; returning takes it off. Only the frame on top runs, so JavaScript does one thing at a time.

Try it

In this example one function calls another. Press Next step and watch frames being added and removed in the Call stack panel, top first.

Test yourself. When this is on, LoopLens pauses before every step that prints to the console and asks what it will print. Pick an answer to check it, or choose Just show me.Step 0 of 10 · virtual clock 0 ms
Event loop:Script (now)MicrotasksTask, then it repeats.The event loop runs the whole script first. Then it runs every microtask, then one task, and repeats.

Code (current line highlighted)

1current line: function greet(name) {
2 return "Hello, " + name;
3}
4
5function welcome() {
6 const msg = greet("Ada");
7 console.log(msg);
8}
9
10welcome();
11console.log("done");

Call stack (top first)The function running right now is on top. Calling a function adds a frame; returning removes it. Only one frame runs at a time.

main script

Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.

empty
The whole script runs as ONE task on the call stack. Lines run top to bottom.

Microtask queue (next first)Promise callbacks (.then, code after await, queueMicrotask) wait here. Once the call stack is empty, all of them run before the next task.

empty

Task queue (next first)Timer callbacks wait here. The event loop takes one task, runs it, then empties the microtask queue before taking the next.

empty

Console output

nothing yet

Scope and variables (yellow = changed this step)

no variables yet

Timeline (click a block or use the arrow keys)Script, function calls and returnsawait and microtasksTimers and tasksConsole outputErrors

Step by step: what the example does

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

function welcome() {
  const msg = greet("Ada");
  console.log(msg);
}

welcome();
console.log("done");
  1. The script starts as the main script frame, at the bottom of the stack. The two function declarations are registered, but nothing inside them runs yet.
  2. welcome() is called on line 10, so a welcome() frame goes on top of the main script.
  3. Inside welcome, line 6 calls greet("Ada"). A greet() frame goes on top. It has its own variable name, and welcome pauses, waiting for the result.
  4. greet returns "Hello, Ada". Its frame is removed, and welcome continues exactly where it stopped, storing the value in msg.
  5. welcome prints Hello, Ada, finishes, and its frame is removed too.
  6. Back in the main script, line 11 prints done. Then the main script finishes and the stack is empty.

What a stack frame holds

Each frame belongs to one function call and remembers:

  • which function is running and the line it has reached;
  • its parameters and local variables (its own execution context);
  • where to continue, and where to send the return value, when it finishes.

Because a new frame is created for every call, two calls of the same function never share local variables, even if they run at the same time in the stack, as in recursion.

Last in, first out

A stack works like a pile of plates: the last frame added is the first one removed. A function can only finish after every function it called has finished. That is why greet above returns before welcome, even though welcome started first.

Recursion: a function calling itself

function factorial(n) {
  if (n <= 1) return 1;          // base case: stop here
  return n * factorial(n - 1);   // waits for the smaller call
}

console.log(factorial(4)); // 24

Each call waits for the next, smaller one, so the stack grows to four factorial frames (n = 4, 3, 2, 1). Then the frames come off one by one, each multiplying the value it got back: 1, 2, 6, 24. Paste this code into the visualizer with Edit code to watch the stack grow and shrink.

Stack overflow: "Maximum call stack size exceeded"

The stack has a size limit. A recursive function without a working base case keeps adding frames until the engine stops it with an error: RangeError: Maximum call stack size exceeded in Chrome and Node.js, or InternalError: too much recursion in Firefox. The fix is almost always a missing or unreachable base case, such as a condition that never becomes true.

One stack, one thing at a time

JavaScript has a single call stack per thread. While a long function is running, nothing else can: not a timer, not a click handler, not a promise callback. Callbacks wait outside the stack until it is empty, and the event loop decides which one runs next.

Common mistakes

  • Thinking a setTimeout callback is on the stack while it waits. It isn't. It only gets a frame when the event loop runs it, after the current code has finished.
  • Recursion without a base case, or with a base case that is never reached (for example counting up when the condition checks for a smaller number).
  • Reading a stack trace upside down. In an error's stack trace, the first line is the function that was on top of the stack when the error happened; the lines below are the functions that called it.

What LoopLens simulates

The order of calls and returns is real: LoopLens runs your code in its own interpreter and records each step. The stack you see is a picture of that interpreter's state, with the top-level code shown as main script. LoopLens stops a run after 3,000 steps or 40,000 operations to keep the page responsive, so very deep recursion ends with LoopLens's own limit message instead of a real "Maximum call stack size exceeded" error.

Next: How the JavaScript event loop works →

Open the visualizer with your own code