Skip to content
wannacode

The call stack from the inside

In short: The call stack keeps one frame for every call that has not finished; when recursion goes too deep it runs out with RangeError: Maximum call stack size exceeded.

Problem

The catalog that takes the page down

The menu of an online shop is a tree: categories, subcategories inside them, a few products in each. Next to the name it says how many products there are, subcategories included: "Electronics (10)". A recursive function counts them, as in the lesson "Recursion":

function countProducts(category) {
let total = category.products;
for (const child of category.children) {
total += countProducts(child);
}
return total;
}
const phones = new Category("Phones", 5);
const laptops = new Category("Laptops", 3);
const electronics = new Category("Electronics", 2).add(phones, laptops);
const kitchen = new Category("Kitchen", 6);
const home = new Category("Home", 4).add(kitchen);
const shop = new Category("Shop", 0).add(electronics, home);
console.log("Products: " + countProducts(shop));
Output
Products: 20

It works for months. Then two new imports from suppliers arrive. The first puts every new category inside the previous one: the chain is 100,000 levels deep. The second, by mistake, puts "Home" inside its own subcategory "Kitchen". In both cases the page fails with RangeError: Maximum call stack size exceeded.

The code is the same, and it is correct. What ran out?

The idea

What one call costs

Every call gets its own memory on the stack. It is called a frame (stack frame) and holds three things:

  • the parameters: here category, the category this call counts;
  • the local variables: total and child, how far the loop has got;
  • the return point: which line of the caller continues when this call finishes.

The frame lives until the call returns. Then it is thrown away and its place is reused. The number of frames alive at once is the depth of the recursion. It is not the number of calls: the walk calls countProducts 6 times, but there are at most 3 frames at once.

In the recursion lesson you already saw the pile of frames for factorial. Every call has its own n and waits for the answer of the one above it:

function factorial(n) {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(3)); // 6
Step by step, in words
  1. The program calls factorial(3).
  2. New memory for factorial with n = 3.
  3. 3 is not 1, so the function calls factorial(2) and waits for the answer.
  4. The same again: factorial(2) calls factorial(1). Each call has its own n.
  5. n is 1: this is the base case. It returns 1 without calling again.
  6. The answer comes back down: 2 * 1 = 2.
  7. And again: 3 * 2 = 6.
  8. factorial(3) returns 6 and the program prints it.

See it

The frames of the walk

Now the same with the shop tree. Watch the slots of each frame and the line the frame below is waiting on:

function countProducts(category) {
let total = category.products;
for (const child of category.children) {
total += countProducts(child);
}
return total;
}
console.log("Products: " + countProducts(shop));
Step by step, in words
  1. The program calls countProducts(shop). So far the stack holds only its frame.
  2. A new frame: the parameter category = Shop, the local total = 0 and the return point, where to go back when it ends.
  3. The loop reaches child = Electronics and calls itself. The Shop frame stops on line 4 and waits; the new frame remembers that line.
  4. Phones has no subcategories: the loop does not run. That is the base case. 3 frames of the walk are alive at once: the depth is 3.
  5. return 5: the frame is thrown away. Electronics carries on from line 4: total = 2 + 5 = 7.
  6. Laptops takes the same place in the stack and returns 3. total = 7 + 3 = 10.
  7. Electronics returns 10. The Shop frame wakes up where it stopped: total = 10.
  8. Next child: Home, then Kitchen. Again 3 frames, in the same places.
  9. Kitchen returns 6, Home returns 4 + 6 = 10, and Shop gets to 10 + 10 = 20.
  10. 20 products. 6 calls, but never more than 3 frames of the walk at once: the stack grows with the depth of the tree, not with the number of categories.

When "Phones" returns, its frame disappears, and "Laptops" takes the same place. The memory of the walk grows with the longest path in the tree, not with the number of categories. For a chain of 100,000 categories the longest path is 100,000.

Try it

How many frames at once?

Here countProducts counts how many of its frames are alive (depth) and remembers the largest value (deepest). The tree is the same: Shop, Electronics with Phones and Laptops, Home with Kitchen.

let depth = 0;
let deepest = 0;
function countProducts(category) {
depth++;
deepest = Math.max(deepest, depth);
let total = category.products;
for (const child of category.children) {
total += countProducts(child);
}
depth--;
return total;
}
const total = countProducts(shop);
console.log(`${total} products, at most ${deepest} frames at once`);
Answer

The idea

Why the stack runs out

The stack is a separate block of memory for each thread, and it does not grow by itself: in Java, C# and Node it is on the order of 1 MB, and Python counts frames and stops at 1000. One frame is small, tens to a hundred bytes, but the chain from the import needs 100,000 frames at once. chain(n) builds such a chain: n categories, each inside the previous one, one product each.

console.log(`Depth 900: ${countProducts(chain(900))} products`);
try {
console.log(`Depth 100000: ${countProducts(chain(100_000))} products`);
} catch (e) {
console.log(`Depth 100000: ${e.name}: ${e.message}`);
}
Output
Depth 900: 900 products
Depth 100000: RangeError: Maximum call stack size exceeded
In Node the stack is about 1 MB. How many calls fit depends on the frame: for countProducts it is about 10,000. 900 levels pass, 100,000 do not.

The memory the walk needs is O(depth), as in the lesson "Space complexity". In a balanced tree the depth is small. In a chain it is n.

See it

The error falls through every frame

The overflow happens in the top frame, 100,000 levels up. From there the error falls like any exception: a frame without catch is thrown away and the error keeps going down. That is how the exception from withdrawing money fell through the frames in the lesson "Throwing exceptions":

withdraw(amount) {
if (amount > this.balance) {
throw new RangeError("Not enough money");
}
this.balance -= amount;
}
function payRent(account) {
account.withdraw(800);
console.log("The rent is paid.");
}
try {
payRent(new Account(500));
} catch (e) {
console.log(`Declined: ${e.message}`);
}
Step by step, in words
  1. The program calls payRent for an account with €500.
  2. payRent calls withdraw(800): a new frame on top.
  3. 800 > 500: the method throws an exception.
  4. The balance does not change: the frame is thrown away without finishing its lines.
  5. payRent has no catch: "The rent is paid." is not printed, and the frame is thrown away.
  6. The catch in the program catches the exception, and the program continues from here.

When the error reaches catch, all 100,000 frames are already gone and the stack is empty again. That is why the program above carries on and prints.

The idea

The cycle: the base case never comes

Where is the base case of countProducts? There is no if. The base case is a category with no subcategories: the loop does not run even once, and the function returns its own products.

The second import put "Home" inside its own "Kitchen". Now the path Shop → Home → Kitchen → Home → Kitchen → … has no end. Every category on it has a subcategory, so the base case never comes:

kitchen.add(home);
try {
console.log("Products: " + countProducts(shop));
} catch (e) {
console.log(`With the cycle: ${e.name}: ${e.message}`);
}
Output
With the cycle: RangeError: Maximum call stack size exceeded

This is a base case that is never reached. It is there in the code, but the input never leads to it. The tree comes from outside (an import, an API, a database), so the walk has to survive both a deep tree and a circular one. How is in the next lesson, "From recursion to a loop".

The idea

Two base cases

Sometimes there is more than one reason to stop. find looks for a category by name and has two base cases:

  • found: the name matches, and the function returns the category without going further down;
  • nothing further down: no subcategory contains it, and the function returns null.
function find(category, name) {
if (category.name === name) {
return category;
}
for (const child of category.children) {
const found = find(child, name);
if (found !== null) {
return found;
}
}
return null;
}
for (const name of ["Kitchen", "Garden"]) {
const found = find(shop, name);
if (found === null) {
console.log(name + ": no such category");
} else {
console.log(`${name}: ${found.products} products`);
}
}
Output
Kitchen: 6 products
Garden: no such category

As soon as one child finds the category, the answer goes straight back up through all the frames, without walking the other children.

Try it

Fill in

What does find return when the category is in none of the subtrees?

function find(category, name) {
  if (category.name === name) {
    return category;
  }
  for (const child of category.children) {
    const found = find(child, name);
    if (found !== null) {
      return found;
    }
  }
  return ;
}

Try it

Find the bug

The shop sells in instalments. months(150, 50) returns 3 months. months(130, 50) should also return 3 (€50, €50 and €30), but it stops with RangeError. Which line is wrong?

Pick the line with the bug.

Exercise

The breadcrumb

Solve this in your editor.

The path to the category. Above the products the shop shows the path to the category: "Shop > Home > Kitchen". Write a recursive function path(category, name) that returns the list of names from the root to the category you look for, or null if it is not there. The two base cases are as in find. Read the name and print the path.

Input
Kitchen
Output
Shop > Home > Kitchen

With the input Garden the output is No such category.

More tasks

  • The deepest category. Write a recursive depth(category): a category with no children is at depth 1, otherwise it is 1 + the deepest child. For the tree in the lesson it is 3, for the chain from the import it is 100,000.
  • The instalments. Fix months and run it with 130 and 50, then with 0 and 50. How many frames does each call open?

More precisely

How big the stack is

The stack is not endless, and it is not the same everywhere. The numbers below are defaults, and they can be changed.

More precisely, if you are curious

The size. In Node the stack is about 1 MB and is changed with node --stack-size=…. A value larger than what the operating system gives the process does not help: the process crashes without a RangeError. In the browser the size is different in every browser.

The size of a frame. How many frames fit depends on the frame: more parameters and local variables are more bytes. That is why in Node the number of calls before the overflow is not an exact number, while in Python it is, because Python counts frames.

The tree and object memory. The categories themselves are not on the stack. They are objects in object memory (the heap), and a frame holds only a reference to one of them. That is why 100,000 categories fit in memory without trouble, while 100,000 frames at once do not.

Returning. The return point is the address of the instruction where the caller continues. On the processor this is literally an address in memory, written into the frame. The scene shows it as a line number.

Test

Check yourself

1. What does the frame of the call for the category "Home" hold?
2. The catalog has 1000 categories, and the longest path from "Shop" to the deepest one is 4 categories. How many frames of the walk exist at once, at most?
3. "Home" is put under its own "Kitchen" by mistake. Why does the walk overflow the stack instead of giving a wrong sum?
4. months(130, 50) should return 3, but it overflows the stack. Why?
5. What does the Python limit measure, and what does the stack limit in Java, C# and JavaScript measure?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·