Before you begin
Why this matters
Every function you call keeps its own values while it runs and loses them when it returns. Knowing this explains a counter that never counts, and it lets you read the error lines Node prints when a program stops.
What this helps you reason about
- Read the stepper’s frame list: which call is running, and where each waiting call continues.
- Spot a value that a function expects to remember from its last call.
- Read the list of calls that an error prints, from the top, to find the function that failed.
This lesson looks at what a call keeps while it runs, how calls pile up and come back, and how to read the list of calls that an error prints.
Already know this? The short version
- A frame holds a call’s parameters, its locals, and where to continue in the caller; the stepper shows the last as the line beside each waiting frame.
- Calls push frames and returns pop them; only the top frame runs, and two calls in sequence never coexist.
- Each call has fresh locals, so a let counter inside a function is 0 at every call; a module-level variable is shared by every call.
- A returning call’s locals are gone; objects it returned stay while they are reachable. Node prints an uncaught error’s stack newest first, so read it from the top.
Words in this lesson (4)
- frame
- The record of one function call that has not finished: its parameters and local variables. It is created when the call starts and removed when the call returns.
- local variable
- A variable declared inside a function with let or const. Each call creates its own, in its frame, and it is gone when the call returns.
- call stack
- The frames of all calls that have not finished yet, with the newest on top. A new call adds a frame on top, and a return removes it. Only the newest call runs; each call below it waits for the call it made.
- stack trace
- The list of calls that were active when an error was created, printed newest first: the function where the error happened, then the function that called it, down to the top level.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Frames
What does a call need to keep?
Function calls showed that a call pauses the code that made it, and later continues at the place of the call. To do that, each call keeps some state while it runs. Picture each call as a frame: the record of one call that has not finished.
A frame holds three things: the call’s parameters, its local variables (the variables declared inside the function), and the place to continue in the caller when the call returns. The stepper shows the first two as the frame’s variables. It shows the third as the line next to each waiting frame: the line of the call that the frame is waiting on.
function applause(pieces: number): number {
const each = 2;
return pieces * each;
}
function concert(pieces: number, music: number): number {
const clapping = applause(pieces);
return music + clapping;
}
const minutes = concert(3, 40); // 46 A frame keeps a call’s parameters, its local variables, and the place to continue when it returns.
Precisely: frames are a model
The language promises that each call has its own variables and that a return continues the caller. It does not promise a frame in memory. The JavaScript engine, V8 in Node.js, may keep values in processor registers or copy a small function’s body into its caller. Where values live in memory is the subject of Stack and heap, a later lesson.
Experiment
Find where each frame will continue
Each call gets a frame with its own parameters and local variables. The frame of the caller stays in the list while the call runs, and it remembers the line where it called. When the newest call returns, its frame is removed, and the frame below continues from that line.
Why it matters Where a call returns to is decided by where it was called from, not by the function. The frame list shows that place for every waiting call.
Predict how many frames the frame list shows while applause runs, and which line each waiting frame shows. Then add the line console.log("bow", applause(1)); as the last line of the program, and predict which frame is under applause this time.
function applause(pieces: number): number {
const each = 2;
return pieces * each;
}
function concert(pieces: number, music: number): number {
const clapping = applause(pieces);
return music + clapping;
}
console.log("doors open");
const minutes = concert(3, 40);
console.log("concert", minutes);
What to look for
Step until the stop inside applause. The frame list shows three frames, newest on top: applause, then concert · line 7, then <top level> · line 12. The line next to each waiting frame is the line of the call it waits on: when applause returns, concert continues on its line, and the caller of concert on its own. concert’s clapping and <top level>’s minutes show not initialized yet: they get values only when the calls return. After the edit, applause appears a second time, with pieces 1, directly on top of <top level> · line 14: the same function, a different place to continue. The run ends with bow 2. <top level> is the stepper’s name for the code outside every function. The panel lists applause and concert as function applause(pieces) and function concert(pieces, music): a function declared with function is also a variable. Step sometimes stops on the same line two or three times; that is normal for this debugger.
Expected output
doors open
concert 4602 / The stack
What is the call stack?
The frames of all unfinished calls form the call stack. A new call adds a frame on top. Only the top frame runs; every frame below it waits for the call above it. A return removes the top frame, and the frame below continues. The last frame added is always the first one removed.
So the stack grows by one frame at each call and shrinks by one at each return. While applause runs, it holds three frames: applause, concert, and <top level>. After both calls return, it holds <top level> alone again. Two calls written one after the other are never on the stack together: the first call’s frame is gone before the second call starts.
A picture of the call stack while the program runs: the newest frame is listed first. It is a model, not a memory layout.
A conceptual model, not a physical memory layout or live code execution.
1 / 5
Start
- stack
- <top level>
Only <top level> is running.
A call adds a frame on top of the stack; a return removes the top frame.
Precisely: deeper stacks
A function that calls itself adds a new frame at each call, and each frame has its own variables. That is the subject of Recursion, a later lesson. The stack has a size limit, and going past it is a stack overflow.
03 / Own variables
Do two calls of one function share their variables?
Every call gets a new frame, so two calls of one function have separate variables, even with the same names. A local variable starts again from its declaration at every call.
This gives the most common frame mistake: expecting a local to remember its value until the next call. encore below counts encores in a local. Every call creates a new encores with 0, so every call prints encore 1.
To keep a value from one call to the next, keep it somewhere that lasts longer than one call. A variable declared outside every function lasts for the whole run, and every call uses the same one.
function encore(): void {
let encores = 0;
encores = encores + 1;
console.log("encore", encores);
}
encore(); // encore 1
encore(); // encore 1 again A local variable is new at every call: it cannot remember anything from the call before.
Coming from C: JavaScript has no static local variables. A value that must keep its value between calls is declared outside the function.
Experiment
Count encores with a local
Each call to encore gets a new frame, and the local encores is created in it, starting at 0. The frame and its encores are removed when the call returns, so the next call cannot see the 1. A variable declared outside the function is not in any frame: every call uses the same one.
Why it matters A local cannot remember anything between calls. A value that must last from one call to the next has to live somewhere that lasts longer than the call.
Predict both printed lines. Then move the line let encores = 0; from inside encore to above function encore, and predict again.
function encore(): void {
let encores = 0;
encores = encores + 1;
console.log("encore", encores);
}
encore();
encore();
What to look for
Step into the first call. encore’s frame lists encores as not initialized yet, then 0, then 1, and the run prints encore 1. The frame is removed. The second call gets a new frame, and encores again starts as not initialized yet, then 0: nothing from the first call is left. The run prints encore 1 twice. After the edit, <top level>’s frame lists encores too, and both frames show the same value. It is already 1 when the second call starts, and the run prints encore 1, then encore 2.
Expected output
encore 1
encore 104 / After the return
What happens to a frame when its call returns?
When a call returns, its frame is removed, and its parameters and local variables go with it. Only the returned value reaches the caller, and the caller stores it in a variable of its own frame.
A returned object or array is different from the local that named it. The local is gone, but the object itself stays as long as something can reach it, such as the caller’s variable. Why is the subject of Stack and heap, a later lesson.
function makeProgramme(): { pieces: number } {
const programme = { pieces: 3 };
return programme;
}
const tonight = makeProgramme();
// programme is gone; tonight.pieces is 3 A return removes the frame’s variables; only the returned value comes back.
05 / Stack traces
How do you read a stack trace?
A stack trace is the frame list, printed. When an error stops a program, the runtime prints the frames that were active at that moment, newest first. throw new Error("no seats sold") creates an error with that message and stops the function. If nothing handles the error, it stops every waiting caller too, and the program.
Read a stack trace from the top. The first line names the error. The next line is the frame where the error happened, with its line number. Each line below is the caller that was waiting on the frame above it. The last line is where the program started; the mistake is rarely there.
When perSeat below is given 0 seats, the stepper shows this trace. perSeat failed at line 3; report called it from line 9; the top level called report from line 13.
Error: no seats sold
at perSeat (line 3)
at report (line 9)
at <top level> (line 13) Read a stack trace from the top: the first frame is where it failed, and each frame below called the one above.
Coming from Python: Python prints the oldest call first and writes most recent call last above its trace. This language prints the newest call first, so the line to read first is at the top.
Precisely: what Node prints
In a terminal, Node prints each file’s path with a line and a column, such as at perSeat (/home/ana/app.js:3:11), and adds lines from Node’s own code below the top level. The stepper shortens the lines to the lesson’s line numbers. The list is recorded when new Error runs, not when the error is thrown.
Experiment
Read a stack trace from the top
When the error happens, the frames of every unfinished call are still in place: the failing function on top, each caller under the call it is waiting on. The runtime prints that list, newest first, and then stops the program.
Why it matters Read a stack trace from the top. The first frame is where the error happened; the frames below show how the program got there. The last frame, where the program started, is rarely where the mistake is.
Predict which function a stack trace names first if perSeat fails. Then change report(300, 4) to report(300, 0) and read the error.
function perSeat(takings: number, seats: number): number {
if (seats === 0) {
throw new Error("no seats sold");
}
return takings / seats;
}
function report(takings: number, seats: number): string {
const price = perSeat(takings, seats);
return "average " + price;
}
console.log(report(300, 4));
What to look for
Before the edit, the run prints average 75, and at the stops inside perSeat the frame list shows perSeat on top of report · line 9 and <top level> · line 13. After the edit, perSeat throws, nothing is printed, and the run stops with Error: no seats sold, followed by at perSeat (line 3), at report (line 9), and at <top level> (line 13). That is the frame list at the moment of the throw, newest first.
Expected output
average 7506 / Try it
Can you follow a fresh example?
Follow the local and the outside variable separately.
let shows = 0;
function bow(): number {
let bows = 0;
bows = bows + 1;
shows = shows + 1;
return bows * 10 + shows;
}
console.log(bow(), bow(), bow());
What is printed?
Reveal the reasoning
11 12 13. bows is a local, so every call starts it at 0 and it is always 1 before the return. shows is outside every frame, so it keeps counting: 1, 2, 3. Each result is bows * 10 + shows.
Practice tickets
- FRAME-101 · The widest jump is always far too large → Free
- FRAME-102 · The evening show’s ice-cream sales include the afternoon’s → Sign in · Pro
- FRAME-103 · A dance class plan is sometimes one bar short → Sign in · Pro