Functions and the call stack: code and trace reading
Crux Read four small TypeScript snippets, trace the stack frame-by-frame, count recursion depth, and predict what parameter passing and return actually do.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min
Reading the call stack from the code is the skill the whole unit builds toward. For each snippet, run it in your head: push a frame on every call, pop one on every return, and watch where the values flow.
Goal
Practise tracing real code the way the machine runs it — counting live frames, predicting recursion depth, and reasoning about what crosses the frame boundary when arguments go in and a value comes back.
Snippet 1 — nested calls and stack depth
function inner(): void { let c = 3; // inner's only local}function outer(): void { let b = 2; // outer's only local inner(); // pushes inner's frame}function main(): void { let a = 1; // main's only local outer(); // pushes outer's frame}main();
Quiz
Completed
At the instant inner is executing let c = 3, how many frames are on the stack and which holds a = 1?
Heads-up outer and main have not returned yet — they are suspended below inner, waiting for it to finish. Their frames remain on the stack, so three frames coexist.
Heads-up a is main's local, so it lives in main's frame at the bottom, not in inner's. Each function's locals live only in its own frame.
Heads-up Frame count is the number of active calls, not callees. main, outer, and inner are all active while inner runs, so there are three frames.
Snippet 2 — parameter passing (pass-by-value)
function bump(x: number): void { x = x + 100; // reassigns bump's own copy of x}function main(): void { let v = 1; bump(v); // argument 1 copied into bump's x // what is v here?}
Quiz
Completed
After bump(v) returns, what is the value of v in main, and why?
Heads-up bump added 100 to its own copy x, not to v. With pass-by-value the argument is copied into the callee's frame, so the caller's variable is never modified by reassigning the parameter.
Heads-up Passing a value as an argument does not consume or remove the caller's variable. main's v keeps its original value 1 throughout.
Heads-up x is initialised to a copy of the argument (1), not 0, so x becomes 101 inside bump — but that lives in bump's frame and never flows back. main's v stays 1.
Snippet 3 — recursion depth
function sumTo(n: number): number { if (n === 0) return 0; // base case return n + sumTo(n - 1); // recursive case}sumTo(4);
Quiz
Completed
For sumTo(4), what is the maximum number of frames live at once, and what value is returned?
Heads-up Each recursive call pushes a new independent frame holding its own n; the n = 4 frame is suspended at 4 + sumTo(3) until the whole chain returns. Five frames coexist at the deepest point.
Heads-up The base-case call sumTo(0) also pushes a frame before returning 0, so the peak is five frames, not four — even though it adds nothing to the sum.
Heads-up The base case contributes 0, but each suspended frame adds its own n as the chain unwinds: 0 + 1 + 2 + 3 + 4 = 10. The base case only stops the descent.
Snippet 4 — missing base case
function blow(n: number): number { return n + blow(n + 1); // no base case: n only grows}blow(0);
Quiz
Completed
What happens when blow(0) runs, and what is the underlying cause?
Heads-up There is no base case and n only increases, so no call ever returns a plain value to start the unwind. Nothing is ever summed — the program crashes before returning anything.
Heads-up Unlike a loop, each recursive call consumes a new stack frame. The stack is finite, so it overflows and crashes within milliseconds rather than spinning indefinitely.
Heads-up Compilers generally do not prove a base case is reachable; this builds fine. The failure is a runtime stack overflow, not a compile error.
Recap
Every snippet is read by running the stack in your head: nested calls keep all callers’ frames live below the active one; a copied argument means reassigning a parameter never touches the caller’s variable; a recursive call pushes a new frame per level so peak depth is the number of nested live calls (including the base case); and a recursion with no reachable base case grows the stack without bound until it overflows. Trace push-on-call, pop-on-return, and the answers fall out.