Before you begin
Why this matters
Knowing when a value stops being usable explains why a function can return an object it created. It also explains why memory can keep growing when you expected it to be freed, for example when a function you stored somewhere keeps using a large object.
What this helps you reason about
- Predict whether a value is still usable after the call that created it returns.
- Recognize when a function you return keeps one of your variables alive.
- Find what still leads to an object when memory keeps growing.
The stack-and-heap picture is a correct, useful model of how calls work, and this lesson starts from it. It uses the picture to predict real behavior, then refines it where JavaScript needs more. TypeScript types are removed before the program runs, so JavaScript rules decide how memory behaves.
Already know this? The short version
- Each call has a frame for its parameters and locals. The frame is removed when the call returns, and only the return value reaches the caller.
- Objects, arrays, and functions are not part of any frame. A returned reference keeps its object usable, and every copy of that reference reaches the same object.
- A closure keeps the variables it uses alive after their call returns. Event listeners and callbacks inside React effects are closures, so a listener that is never removed keeps everything it uses alive.
- The garbage collector reclaims a value some time after it becomes unreachable from running calls and globals, cycles included. You never free memory yourself.
- V8 may keep values in registers or skip allocations it can prove are unneeded, so measure memory with a heap snapshot instead of reasoning from a drawing.
Words in this lesson (8)
- 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.
- 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.
- heap
- An area of memory for values that must outlive the call that created them. In JavaScript, objects, arrays, and functions are kept there.
- primitive
- A simple value that is not an object: a number, string, boolean, bigint, symbol, undefined, or null. A primitive cannot be changed; operations on it make new values.
- reference
- A value that leads to an object. A variable whose value is an object holds a reference, not a copy of the object, so copying the variable gives a second way to reach the same object.
- closure
- A function together with the variables from the surrounding code that it uses. Those variables last as long as the function does.
- reachable
- A value is reachable when the program can still get to it by following references from the variables of running calls or from global variables.
- garbage collector
- The part of the JavaScript engine that finds values that are no longer reachable and reuses their memory, at a time it chooses.
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 / The common picture
What do people mean by stack and heap?
Here is the picture most programmers learn. It is a teaching model: a simple picture that explains real behavior. Each function call gets a frame, a block of memory for its parameters and local variables. The frames are kept on the stack, also called the call stack. A new call adds a frame on top of the stack, and returning removes that frame.
Some values must stay usable after the call that created them returns, for example an object that the call gives back to its caller. A frame cannot hold such a value, because the frame is removed when the call returns. So those values are kept in a separate area of memory, called the heap, until the program no longer needs them.
A primitive is a simple value that is not an object, such as a number, a boolean, or a string. In the picture, small primitives such as numbers and booleans are drawn inside the frame. Objects, including arrays and functions, are drawn on the heap.
A string is a primitive too, so it behaves like a number: it cannot be changed, and copying it never links two names. Its text can be long, so in practice the text is stored outside the frame. To predict what a program does, treat a string like a number.
This model is correct for many programs. The rest of this lesson uses it to predict behavior, then refines it for the cases it does not cover.
Frames hold one call’s variables; objects, arrays, and functions live on the heap, so they can still be used after the call that made them returns.
Precisely: where values are stored
The engine is the program that runs your JavaScript, such as V8 in Node.js and Chrome. V8 stores string text in its heap and keeps a reference to it in the frame; numbers that are not small integers can be stored there too. The ECMAScript specification does not say where any value is stored. It only defines how values behave.
02 / Frames
What happens to a call’s variables when it returns?
A frame exists only while its call is running. When inner returns, its frame is removed and doubled is gone. outer receives only the returned value.
function inner(n: number): number {
const doubled = n * 2;
return doubled;
} A call’s local variables end when it returns; its return value does not.
Experiment
Watch a frame come and go
Each call gets a frame holding its parameters and local variables. A frame exists only while its call is running. When inner returns, its frame is removed; only the returned value reaches outer.
Why it matters Local variables belong to one call. Once the call returns, the only thing the caller has is the value that was returned.
Predict what prints. Step until inner is running, then step once more. What happens to doubled?
function inner(n: number): number {
const doubled = n * 2;
return doubled;
}
function outer(): number {
const start = 4;
const result = inner(start);
return result + 1;
}
console.log(outer());
What to look for
Watch the frame list. outer appears, then inner on top of it. When inner returns, its frame and its doubled variable are gone, and outer continues with result. Each frame also lists functions such as inner as variables, shown by name and parameters, such as function inner(n): that is how the panel shows a function, not a copy of it. Node can stop on the same line two or three times; that is normal for this debugger.
Expected output
903 / Outliving a call
How can a value last longer than the call that made it?
A reference is a value that leads to an object, and the Variables lesson showed that copying one gives a second name for the same object. This section asks how long that object lasts.
An object created inside a call is not part of the frame. When makeCounter returns, its frame and its value variable are gone, but the object stays, because first still refers to it. A second call creates a second object; it does not reuse the first one.
function makeCounter() {
const value = { count: 3 };
return value;
}
const first = makeCounter();
first.count = 9;
const second = makeCounter(); // a new object: count is 3 Two returned objects. This is a model of which names reach each object, not of memory addresses.
A conceptual model, not a physical memory layout or live code execution.
1 / 3
Create
- makeCounter: value
- refers to A
- A.count
- 3
value in makeCounter refers to object A.
An object lasts as long as the program can still reach it, not as long as the call that created it.
Experiment
Keep an object after its call returns
The object is created inside makeCounter, but it does not belong to the frame. Each call creates a new object and returns a reference to it. An object stays usable as long as the program can still reach it.
Why it matters A function can safely return objects it created, and each call gives a new one. Memory for an object is reclaimed some time after nothing the program can still reach refers to it, not when the function returns.
Predict both numbers: does the second call give back the first call’s object, or a new one? Then change const second = makeCounter(); to const second = first; and predict again.
function makeCounter() {
const value = { count: 3 };
return value;
}
const first = makeCounter();
first.count = 9;
const second = makeCounter();
console.log(first.count, second.count);
What to look for
Step past the first return. The makeCounter frame is gone, but first still shows {count: 3}, then {count: 9}. The second call starts a new frame that makes a new object, so second shows {count: 3}, and 9 3 prints. After the edit, makeCounter runs once, second shows {count: 9}, and 9 9 prints.
Expected output
9 304 / Refining the picture
Where does the picture need refining?
Here the picture needs refining. count is a local variable of makeCounter, so the picture says count ends when makeCounter returns. But the returned function still reads and changes count.
A function that uses variables from the code around it is called a closure. When a closure outlives the call, the variables it uses outlive the call too. JavaScript keeps them alive for as long as the closure exists.
Any function you store for later is a closure in the same way, for example a function that other code will call when something happens. The variables that such a function uses stay alive. They are released only after the function itself is removed.
function makeCounter(): () => number {
let count = 0;
return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2 A local variable can outlive its call when a function still uses it.
Coming from Java: A Java lambda cannot assign to a captured local, but a JavaScript closure can, and every closure from the same call sees the change.
Coming from React: Every event listener, and every callback inside a useEffect, is a closure. The variables it uses stay alive until the listener or callback is removed, which is why an effect that adds a listener needs a cleanup function that removes it.
Coming from React: A closure also keeps the variables of the render that created it. An effect whose dependency list leaves out count keeps reading the count from an old render: a stale closure. List every value the effect reads in its dependencies, or use the updater form, setCount((c) => c + 1).
Experiment
Keep a local alive in a closure
increment uses count, so count must last as long as increment does. A local variable that a returned function uses is not stored only in the frame.
Why it matters The picture “locals live in the frame” needs refining here. A local can outlive its call when a function that uses it outlives the call.
Predict all three numbers. Then change console.log(next(), next(), next()) to console.log(next(), makeCounter()(), next()) and predict again. makeCounter() returns a new increment function, and the second () calls it at once.
function makeCounter(): () => number {
let count = 0;
return function increment(): number {
count += 1;
return count;
};
}
const next = makeCounter();
console.log(next(), next(), next());
What to look for
After makeCounter returns, step into increment. Its frame shows count, even though the makeCounter frame that declared count is gone. After the edit, 1 1 2 prints: the middle call has its own count, and next keeps counting with the first one.
Expected output
1 2 305 / Releasing memory
Who decides when memory can be reused?
You never free memory yourself in JavaScript. Instead, the engine has a garbage collector: a part of the engine that finds memory the program can no longer use and makes it available again.
A value is reachable when the program can get to it by following references from the variables of calls that are still running, or from global variables. The garbage collector reuses the memory of values that are no longer reachable, at a time it chooses. Two objects that refer only to each other are not reachable, so they are reclaimed too.
So memory stays in use as long as something reachable still refers to an object: a variable, another object, a closure, an event listener that was never removed, or a cache. When memory keeps growing, look for references that are still held.
Memory is reclaimed some time after nothing the program can still reach refers to a value.
Coming from Python: CPython frees most objects as soon as their last reference goes away; V8 does not count references, so an unreachable object is reclaimed only when the garbage collector next runs.
06 / The implementation
What may the compiler or runtime change?
The language only promises how values behave. It does not promise where values are stored.
While your program runs, Node.js may store a value somewhere other than the picture shows, or skip creating an object that is never needed. Your program’s results are the same either way; only its speed and memory use change.
So a stack-and-heap drawing cannot tell you how much memory a program really uses. To answer that, measure a real run.
Precisely: the JIT, and evidence from a real run
V8, the engine inside Node.js, contains a just-in-time compiler, or JIT: it translates often-used JavaScript into machine instructions while the program runs. That code may keep a number in a register, a small storage place inside the processor, instead of in a frame, and may skip creating an object that never leaves its function. A heap snapshot from the browser or Node.js developer tools shows which objects are reachable and through which references, for one engine and one run. That is better evidence for a memory question than a stack-and-heap drawing.
07 / Try it
When can each value’s memory be released?
Predict the output, then follow each count: which one can the program still reach after line 12?
function makeCounter(): () => number {
let count = 0;
return function next(): number {
count += 1;
return count;
};
}
let tally = makeCounter();
tally();
tally();
tally = makeCounter();
console.log(tally()); What prints? After line 12, which count can the program still reach, and when can the memory of the other one be reused?
Reveal the reasoning
1. Each call to makeCounter creates a new count and a new next function that uses it. Line 12 stores the second next in tally; its count starts at 0, so 1 prints. After line 12, nothing the program can still reach refers to the first next function, so it and its count, which had reached 2, are unreachable. The garbage collector may reuse that memory at a time it chooses, not at line 12. Both makeCounter frames ended earlier, when each call returned, but neither count ended with its frame.
Practice tickets
- STK-101 · Keep each shelf label as it was printed → Free
- STK-102 · Hand out a receipt snapshot that stays as it was → Sign in · Pro
- STK-103 · Open one connection and close it → Sign in · Pro
Keep exploring
Investigate a minimal reproduction →