The call stack
Learn how JavaScript tracks function calls by pushing and popping stack frames, how to read stack traces, why stack overflow happens, and how DevTools shows the stack while debugging.
- 01Follow callsSay which frame is on top of the call stack, and why the functions that called it wait underneath.
- 02Read a stack traceUse a stack trace to find where an error started and which calls led there.
- 03Debug with the stackSpot a stack overflow, use the Call Stack panel in DevTools, and name your functions so traces are easier to read.
The stack of calls
JavaScript runs one piece of code at a time. That code might be at the top level of a script, which means it is not inside any function. It might be inside a function. Or it might be inside a function that another function called.
The call stack is the list JavaScript keeps of the function calls that are running right now. A stack is a pile where you add things on top and take them off the top.
Each entry in the call stack is called a stack frame, or just a frame. One frame stands for one call. In the last lesson, Execution contexts, you saw what a call needs to run: its parameters, its local variables, and links to outer scopes. The frame holds on to that context. It also remembers where to go back to when the call returns.
function greet(user) {
console.log("Hello " + user);
}
greet("Asha");Line 1 creates greet. It doesn't run yet. Line 4 calls it with "Asha". So JavaScript puts a frame for greet on top of the stack. Putting a frame on top is called a push.
greet prints Hello Asha and returns. Then its frame comes off the top. Taking a frame off is called a pop. After that, the script goes on.
greet("Asha"), a new frame for that call goes on top of the pile. This is a push. When greet returns, its frame comes off the top. This is a pop. The script frame at the bottom then goes on.Each time you start a task, you write a sticky note. The note says what you are doing and where to go back to. You stick the newest note on top of the pile. When you finish that task, you tear off the top note. Then you go back to the note underneath.
- In real life: Stick a new note on top when you start a task
- In JavaScript: A call pushes a new frame on top
- In real life: The top note is the task you are doing now
- In JavaScript: The top frame is the function running now
- In real life: Tear off the top note when that task is done
- In JavaScript: A return pops the top frame off
- In real life: Photocopy the pile when something goes wrong
- In JavaScript: A stack trace saves the list of frames when an error happens
The Recursion lesson used a pile of plates to show a function that calls itself. This lesson uses ordinary functions. You will:
- watch helper functions push and pop frames,
- read a stack trace from top to bottom,
- cause a stack overflow on purpose, safely,
- and use the Call Stack panel in DevTools.
Adding and removing frames
STEP THROUGHEach time JavaScript calls a function, it pushes a frame. The function that made the call is called the caller. The caller stops and waits underneath. When the called function returns, its frame is popped. Then the caller goes on from the line where it stopped.
The checkout example below uses a few small helper functions:
checkoutcallscalculateTotal, thenapplyCoupon, thenformatReceipt.calculateTotalandapplyCouponboth callroundPrice.
So roundPrice runs twice, for two different callers. Each of those calls gets its own frame. One function can have many frames: one for each call.
A call puts a frame on top. A return takes it off. Change the coupon to see the middle part take a different line.
script
function checkout(cart, coupon) { const subtotal = calculateTotal(cart); const discounted = applyCoupon(subtotal, coupon); const receipt = formatReceipt(discounted); return receipt;}function calculateTotal(cart) { const total = cart.items.reduce((sum, item) => sum + item.price, 0); return roundPrice(total);}function applyCoupon(total, coupon) { if (coupon === "SAVE10") { return roundPrice(total * 0.9); } return roundPrice(total);}function formatReceipt(total) { return "$" + total.toFixed(2);}function roundPrice(value) { return Math.round(value * 100) / 100;}roundPrice. Only the top frame runs. Each frame below it is a caller that waits for the frame above it to return.A stack frame stands for one call that is still running. It points to that call's execution context. The context holds the parameters, the local variables, and the links to outer scopes. The frame also remembers where to go back to when the call returns. Two later lessons, Stack frames & calling conventions and Stack limits & stack overflow, show what engines store inside a frame.
Watch the stack grow and shrink
INTERACTIVEThe step-through above shows the code line by line. The playground below shows the same run as a pile of cards. Each card is one frame, and the newest card is at the top. The cards are drawn from the steps of the checkout run. They don't show the engine's real memory.
function checkout(cart, coupon) { const subtotal = calculateTotal(cart); const discounted = applyCoupon(subtotal, coupon); const receipt = formatReceipt(discounted); return receipt;}function calculateTotal(cart) { const total = cart.items.reduce((sum, item) => sum + item.price, 0); return roundPrice(total);}function applyCoupon(total, coupon) { if (coupon === "SAVE10") { return roundPrice(total * 0.9); } return roundPrice(total);}function formatReceipt(total) { return "$" + total.toFixed(2);}function roundPrice(value) { return Math.round(value * 100) / 100;}checkout({ items: [{ price: 19.99 }, { price: 5 }] }, "SAVE10");checkout
Push: checkout. The script calls checkout. Its frame is the first function frame.
In a cafeteria, clean trays sit in a pile. You put new trays on top, and you take trays from the top. You can't pull a tray out of the middle without lifting the ones above it. The last tray in is the first tray out.
- In real life: Put a tray on top
- In JavaScript: Call a function, and a frame goes on top
- In real life: Take the top tray first
- In JavaScript: The newest call returns first
- In real life: The bottom tray waits
- In JavaScript: Older callers wait underneath
Reading stack traces
STACK TRACEA stack trace is a copy of the call stack, saved at the moment an error is made. It lists the frames that were on the stack at that moment.
An engine is the part of a browser or of Node.js that runs JavaScript. Chrome and Node.js use V8, Firefox uses SpiderMonkey, and Safari uses JavaScriptCore. Each engine writes a stack trace a little differently. But you read every trace the same way:
- Start at the top. The first frame is where the error started.
- Read down. Each frame below is the function that called the one above it.
function checkout() { calculateTotal();}function calculateTotal() { applyCoupon();}function applyCoupon() { throw new Error("Coupon database is offline");}checkout();Click Show stack trace to see the trace for this code.
Not measured yet.
Show stack trace prints the trace that Chrome and Node write for this code. Other browsers write it a little differently. Measure stack depth really runs in your browser: it counts how many calls fit before the stack is full.
The table below takes that stack trace apart, one line at a time. Read each row as: "this line of the trace means this, and here is how to use it."
| Line in the trace | What it means | How to use it |
|---|---|---|
Error: Coupon database is offline | The error's name and its message. | Read it first, so you know what went wrong. |
at applyCoupon (checkout.js:8:9) | The top frame, the newest call. The error was made on line 8, at character 9. | Look here first. This is usually where the error started. |
at calculateTotal (checkout.js:5:3) | The function that called applyCoupon, from line 5. | Keep reading down to see which calls led to the error. |
at checkout.js:10:1 | The top level of the script, on line 10. It has no function name. | This bottom frame is the oldest call in the trace. |
Error: Coupon database is offline at applyCoupon (checkout.js:8:9) at calculateTotal (checkout.js:5:3) at checkout (checkout.js:2:3) at checkout.js:10:1at line is the newest call. That is where the error started. Each line below it is the function that called the line above. So you read the trace from the top down.function saveProfile() {
validateProfile();
}
function validateProfile() {
console.trace("profile validation");
}
saveProfile();console.trace() prints a stack trace at the line where you call it. Nothing has to throw. In this example, line 5 calls it inside validateProfile. So the trace shows validateProfile on top and saveProfile under it. Each browser and Node print it in their own format, so your output may look a little different.
A function with no name is called an anonymous function. Anonymous functions make traces harder to read, because the trace has no name to show for them. So give your callbacks names. For example, write function showItem(item) { ... } as the callback. Then the trace shows showItem.
V8 also has extra tools that are not part of the JavaScript standard, such as Error.captureStackTrace and Error.stackTraceLimit. The later lesson Stack traces & the stack trace API covers them.
Stack overflow
MEASUREThe stack has a size limit. If calls keep going on top and none of them return, the stack fills up. This is called a stack overflow.
- Chrome, Node, and Safari throw
RangeError: Maximum call stack size exceeded. - Firefox may throw
InternalError: too much recursion.
How many calls fit depends on the engine, the size of each frame, and the settings.
A normal pile of notes grows and shrinks as you start and finish tasks. Now imagine you keep starting new tasks and never finish one. The pile only grows. Soon it reaches the ceiling, and there is no room for one more note.
- In real life: Every new note makes the pile taller
- In JavaScript: Every call adds a frame
- In real life: No note is ever torn off
- In JavaScript: No call returns, so no frame comes off
- In real life: The pile reaches the ceiling
- In JavaScript: The engine throws a stack overflow error
The first example uses a setter. A setter is a function that runs when code assigns to a property. Writing set name(value) { ... } inside an object means: when code writes user.name = something, run this function.
const user = { set name(value) { this.name = value; }};try { user.name = "Ada";} catch (error) { console.log(error.name);}Line 7 writes user.name = "Ada", so the setter runs. But line 3, inside the setter, writes to this.name again. That runs the same setter again, which writes again, and so on. The calls never end. The catch on line 8 catches the error, and line 9 prints its name, RangeError.
To fix it, store the value under a different name, such as this._name, or in a private field.
function first() { second();}function second() { first();}try { first();} catch (error) { console.log(error.name);}Recursion means a function that calls itself. Here, two functions take turns: first calls second, and second calls first. This is called mutual recursion. It overflows for the same reason: no call ever returns.
Line 8 starts the loop inside a try block. When the stack is full, the catch catches the error, so the page keeps working. Line 10 prints RangeError.
first calls second, and second calls first. No call ever returns, so no frame comes off. The pile grows until the engine has no room left. Then it throws a RangeError.A stack overflow doesn't need a function that calls itself. Any chain of calls that never ends can cause it, as long as each call happens right away. For example:
- a setter that writes to its own property,
- a getter that reads its own property,
- or two functions that call each other forever.
The stack in DevTools
TRY ITIn the lesson Debugging in the browser, you paused code with breakpoints. A breakpoint is a line where DevTools pauses your code. You also stepped into and out of functions.
While the code is paused, DevTools shows a Call Stack panel. It is a live view of the call stack. The top entry is the function running now. The functions that called it are below.
function checkout() { calculateTotal();}function calculateTotal() { applyCoupon();}function applyCoupon() { debugger;}checkout();- Open DevTools, paste the code into the Console, and press Enter.
- The
debugger;statement on line 8 pauses the code. DevTools opens the Sources panel (called Debugger in Firefox). - Find the Call Stack panel. It lists
applyCoupon, thencalculateTotal, thencheckout. - Click each one. The Scope panel changes to show that frame's variables.
Some calls happen later, not right away. setTimeout asks the browser to run a function later. The function you hand to it is called a callback.
const logs = [];
function start() {
logs.push("start");
setTimeout(() => {
logs.push("callback");
console.log(logs.join(", "));
}, 0);
logs.push("end");
}
start();- Line 10 calls
start, and line 3 adds"start"to the list. - Line 4 hands the callback to
setTimeout. The callback doesn't run yet. - Line 8 adds
"end". Thenstartreturns, and its frame comes off. - Later, the browser calls the callback on lines 5 and 6. The
startframe is gone by then, so the callback starts on an empty stack. It printsstart, end, callback.
DevTools can still show which code set up the timer. This is called an async stack trace. DevTools builds that history for you. It is not one real stack, and the old frames are not still there. You will meet async stack traces again in the lesson Async stack traces.
- A function call starts running
- A function reaches
return - A function throws an error and has no
catchof its own forEachcalls your callbackconst total = 3 + 4runs- You click an older frame in DevTools
Sort each event by what it does to the call stack.
Where you'll use this
You use the call stack whenever you debug code that jumps between helper functions. Say an error happens inside roundPrice. The stack tells you whether the bad value came from calculateTotal or from applyCoupon. Or say a callback fails inside forEach. The stack tells you which of your functions started the forEach.
The four tools below are easy to mix up. Read each row as: "use this tool when you want to know this."
| Tool | What question it answers | When to use it |
|---|---|---|
| Call stack | Where am I, and which functions called me? | When you step through functions that call other functions. |
| Stack trace | Where did this error start? | When you read error logs and bug reports. |
| Console logs | What value did this line make? | For quick checks while you write code. |
| Breakpoints | What does the program know right now? | When you want to pause and look at the variables. |
When you meet an error, try these steps:
- Find the first frame that is in your own code, and read that line.
- Read down the stack to find which function passed in the bad value.
- Look at the variables in that frame, instead of guessing.
- Run the same path again with a breakpoint on the line where the value changes. Named callbacks make the path easier to follow.
The stack shows the path through your functions. It doesn't tell you whether a value is right. So use it together with a breakpoint, or a console.log of the value you care about.
Common misconceptions
"The stack keeps every variable forever."
No. A frame only stays while its call is running. When the function returns, its frame is gone. A closure can keep some of its variables alive, but not the frame.
"A stack trace lists the oldest call first."
No. Most JavaScript traces list the newest call first. Read down to go back through the callers.
"Throwing an error leaves the frames on the stack."
No. A throw removes frames one by one until a catch handles the error. If nothing catches it, the program reports the error.
"A function can only have one frame."
No. Each call gets its own frame. A function that calls itself can show up on the stack many times.
"Clicking a frame in DevTools goes back in time."
No. Clicking an older frame only changes what you look at. It doesn't run the program again or undo anything.
function loadCart() {
console.log("before");
throw new Error("offline");
}
try {
loadCart();
} catch (error) {
console.log("caught");
}Line 6 calls loadCart. Line 2 prints "before", and line 3 throws an error. loadCart has no catch of its own, so JavaScript removes its frame and keeps looking. It finds the catch on line 7, and line 8 prints "caught".
The stack trace saved in the error still lists loadCart. But that frame is no longer on the stack.
Practice: read the stack
5 EXERCISESWrite what the console prints, in order.
function outer() {
console.log("outer start");
inner();
console.log("outer end");
}
function inner() {
console.log("inner");
}
outer();The output is outer start, then inner, then outer end. The outer frame waits underneath while inner is on top.
Read the stack trace. Which function did the error start in?
Error: Coupon database is offline
at applyCoupon (checkout.js:8:9)
at calculateTotal (checkout.js:5:3)
at checkout (checkout.js:2:3)
at checkout.js:10:1The error started in applyCoupon. The lines below show that calculateTotal called it, and checkout called calculateTotal.
The setter writes to this.name, which runs the setter again. Which property should it write to instead?
const user = {
set name(value) {
this.name = value;
}
};
try {
user.name = "Ada";
} catch (error) {
console.log(error.name);
}const user = {
_name: "",
set name(value) {
this._name = value;
}
};The setter must not write to its own property. this._name is an ordinary property, not a setter. Writing to it calls nothing, so the calls stop.
Click Measure stack depth in the playground in the Reading stack traces section. Round the number to 1000, 10000, or 100000, whichever is closest, and type it.
There is no single right number. Each browser has its own limit. What to remember: don't write code that needs thousands of calls inside calls at the same time.
Which callback name will appear in the stack trace?
function reportFailure() {
["bad"].forEach(function showItem(item) {
throw new Error("Cannot read " + item);
});
}
try {
reportFailure();
} catch (error) {
console.log(error.stack.includes("showItem"));
}The callback is a function with a name: function showItem(item). When it throws, the stack trace shows showItem. That is much easier to search for than a function with no name.
Quiz: check your understanding
7 QUESTIONSFor each question, ask two things. Which frame is on top right now? And when does the function underneath go on? Each answer explains why it is right or wrong.
Question 1 of 7What happens when
checkout()callscalculateTotal()?Choose an answer to see the explanation.
Question 2 of 7What does this program print?
Read the code, then predictfunction outer() { console.log("outer start"); inner(); console.log("outer end"); } function inner() { console.log("inner"); } outer();Choose an answer to see the explanation.
Question 3 of 7In a stack trace from Chrome or Node, which frame comes first, right after the error message?
Choose an answer to see the explanation.
Question 4 of 7Why can the same function name appear twice in a stack?
Choose an answer to see the explanation.
Question 5 of 7The setter on
usercalls itself until the stack is full. What error do Chrome, Node, and Safari throw?Choose an answer to see the explanation.
Question 6 of 7What does clicking a frame in the DevTools Call Stack panel do?
Choose an answer to see the explanation.
Question 7 of 7What does this program print? The callback has a name,
showItem.Read the code, then predictfunction reportFailure() { ["bad"].forEach(function showItem(item) { throw new Error("Cannot read " + item); }); } try { reportFailure(); } catch (error) { console.log(error.stack.includes("showItem")); }Choose an answer to see the explanation.
Key takeaways
- Calling a function pushes a frame onto the stack. Returning pops it off.
- The top frame is the function running now. The functions that called it wait underneath.
- A stack trace is a copy of the stack, saved when an error is made. Read it from the top down.
- A stack overflow happens when calls keep going inside calls until the stack is full.
- When code is paused, DevTools lets you click each frame to see its line and its variables.
One-liner.
The call stack is JavaScript's pile of running function calls: the last call in is the first one out.
Up next: Hoisting & the temporal dead zone.