Skip to content
Ameen Abozaid
PublishedBEG-014-L08

Demo: tracing recursion on the call stack

  • Lesson 8 of 8
  • 12 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

Lesson 6 gave you the rule for recursion and Lesson 5 gave you the call stack. This demo puts them together: you will watch factorial(4) pile four frames onto the stack and then unwind them, first with print statements and a stack dump (which work on any machine), then with a debugger's call-stack view. At the end you will remove the base case on purpose and see exactly what an endless recursion looks like, then restore it.

The recipe-card picture: a cook following the card "peel the pile" stops mid-way to start the same card for a smaller pile, and that cook stops to start it again, and so on. The cook cannot finish a card until the card above it is done. Watching the pile of half-finished cards grow, and then finish from the top down, is the whole idea of recursion.

This is a hands-on lesson. Debugger menus and panel names differ between IDEs and versions, so where I describe clicks, treat the wording as approximate and look for the equivalent in your tool. The Lab (BEG-014-LAB1) asks you to write a recursive factorial, so finish this lesson with a version that works.

Learning Objectives

By the end of this lesson you will be able to:

  • Trace a recursive method with a table of frames and their variables.
  • Use print statements and a stack dump to observe the order of recursive calls and returns.
  • Use a debugger's breakpoint, step-into and call-stack view on a recursive method.
  • Identify the deepest point of the recursion and what the stack holds there.
  • Recognise the repeated line pattern of a StackOverflowError caused by a missing base case.
  • Restore a correct base case and verify the fix.

Written Explanation

The program we trace

We use a slightly expanded factorial so each step has a visible line:

java
static int factorial(int n) {
    if (n <= 1) {
        return 1;                       // base case
    }
    int smaller = factorial(n - 1);     // recursive call
    return n * smaller;                 // runs only after the call returns
}

Splitting the work into smaller and the final return is purely for observation: it gives you a line to stop on after each recursive call comes back.

Step 1: trace by hand with a table

Before running anything, predict. Every call is a frame with its own n; smaller only gets a value when the call below it returns.

EventStack (top first)Notes
main calls factorial(4)factorial(n=4), mainsmaller not set
call factorial(3)factorial(3), factorial(4), main
call factorial(2)factorial(2), factorial(3), factorial(4), main
call factorial(1)factorial(1), factorial(2), factorial(3), factorial(4), maindeepest point: base case, returns 1
factorial(1) returnsfactorial(2) (smaller = 1), ...computes 2 * 1, returns 2
factorial(2) returnsfactorial(3) (smaller = 2), ...computes 3 * 2, returns 6
factorial(3) returnsfactorial(4) (smaller = 6), maincomputes 4 * 6, returns 24

The answer is 24, and at the deepest point there are four factorial frames above main.

Step 2: watch it with print statements

You do not need a debugger to see the pile. Add a line on entry and one on exit, plus a stack dump at the base case:

java
static int factorial(int n) {
    System.out.println("enter n=" + n);
    if (n <= 1) {
        Thread.dumpStack();             // prints the stack right now
        return 1;
    }
    int smaller = factorial(n - 1);
    int result = n * smaller;
    System.out.println("leave n=" + n + " result=" + result);
    return result;
}

(These println calls are temporary tracing tools, so they do not break Lesson 2's rule: delete them afterwards.) Read the real output in Example 1 below.

Step 3: use a debugger

A debugger runs your program while letting you pause it. The steps below describe the common IDE workflow; names differ per tool, so adapt them.

  1. Set a breakpoint on the first line of factorial (in many IDEs by clicking in the margin beside the line number). A breakpoint tells the debugger to pause there.
  2. Start in debug mode (often a bug icon or a "Debug" entry next to "Run").
  3. When it pauses, find the call stack panel. IDEs call it "Frames", "Call Stack" or similar. You should see factorial above main, and a variables panel showing n = 4.
  4. Resume to the next breakpoint hit (or use "step into" at the recursive line). Each time the breakpoint hits again, another factorial frame is on top, with n = 3, then 2, then 1.
  5. Click each frame in the panel: the variables panel changes to show that frame's n. This is Lesson 4's "each call has its own variables" made visible.
  6. Keep stepping (or remove the breakpoint and set one on return n * smaller;). Watch the frames disappear one at a time from the top as their values return: 1, 2, 6, 24.

Note on the environment: I could not drive a debugger in the writing environment, so the debugger steps above come from the general behaviour of IDE debuggers rather than a live session. The print and stack-dump output below was run for real.

Step 4: the planned mistake, with the base case removed

Delete the base case:

java
static int factorial(int n) {
    return n * factorial(n - 1);
}

Run it. There is no frame that ever returns. The stack grows until it overflows, and Java reports StackOverflowError with a trace in which one line repeats. Compare the repeating line to the single-line chain you saw for a, b, c in Lesson 5. With a debugger you would see the same thing: the call-stack panel growing deeper with n going 4, 3, 2, 1, 0, -1, -2 and so on, never reaching a stopping point.

Finally, restore the base case and run again to confirm the answer 24. Always finish the demo by making the fix; a bug you understood but did not fix teaches less than one you closed.

Examples

Example 1: enter, dump, leave (simple)

java
public class Deep {
    static int factorial(int n) {
        System.out.println("enter n=" + n);
        if (n <= 1) {
            Thread.dumpStack();
            return 1;
        }
        int smaller = factorial(n - 1);
        int result = n * smaller;
        System.out.println("leave n=" + n + " result=" + result);
        return result;
    }

    public static void main(String[] args) {
        System.out.println(factorial(4));
    }
}

Output (JDK 21):

text
enter n=4
enter n=3
enter n=2
enter n=1
java.lang.Exception: Stack trace
	at java.base/java.lang.Thread.dumpStack(Thread.java:2209)
	at Deep.factorial(Deep.java:5)
	at Deep.factorial(Deep.java:8)
	at Deep.factorial(Deep.java:8)
	at Deep.factorial(Deep.java:8)
	at Deep.main(Deep.java:15)
leave n=2 result=2
leave n=3 result=6
leave n=4 result=24
24

The four enter lines are the pile growing. The stack dump at the base case shows the pile at its deepest: after the library line, one factorial frame at line 5 (the base case, n = 1), three frames waiting at line 8 (the recursive call, for n = 2, 3, 4), and main at the bottom. The leave lines are the unwinding, in reverse order: 2, 6, 24. There is no leave line for n = 1 because it returned from the base case before reaching that println. Everything matches the hand-made table.

Example 2: the sum of digits as a second trace (practical)

Trace sumOfDigits(472) from Lesson 6 yourself, with the same table style, before reading on:

java
static int sumOfDigits(int n) {
    if (n < 10) return n;
    return n % 10 + sumOfDigits(n / 10);
}

The frames, top first, at the deepest point: sumOfDigits(4), sumOfDigits(47), sumOfDigits(472), main. The base case returns 4. The frame for 47 then computes 7 + 4 = 11, and the frame for 472 computes 2 + 11 = 13. The answer is 13. Notice the pattern shared with factorial: the stack goes down by repeated shrinking (here, dropping the last digit) and the answers are assembled on the way back up. Once you can draw this, you understand recursion.

Example 3: the missing base case in a trace (realistic)

With the base case deleted from factorial and factorial(4) called, a real run on JDK 21 began like this:

text
Exception in thread "main" java.lang.StackOverflowError
	at NoBase.factorial(NoBase.java:3)
	at NoBase.factorial(NoBase.java:3)
	at NoBase.factorial(NoBase.java:3)
	... (the same line repeated; in my run 1,024 lines were printed)

How to read it quickly: (1) the error name tells you the stack is exhausted; (2) the same method and line repeating tells you the method is calling itself without stopping; (3) so look for the base case, and for whether each call moves toward it. Here the diagnosis takes seconds because you recognise the pattern. In a large program, the repeating lines might be a loop of two or three different methods calling each other, the same bug in a longer disguise.

Backend Insight

When a production service logs a StackOverflowError, the log contains this same repeating trace. The skill from this demo, spotting the repeated frame and asking "where is the base case?", is what lets you find the cause in the log without reproducing it. The fix is in the logic (add or correct the base case, or break a cycle in the data), not in the server settings.

More generally, stepping through code with a debugger and reading the call-stack panel is a daily backend skill: it shows who called your method and with which values, which is often quicker than adding print statements to a service that is hard to restart.

Common Mistakes

Mistake 1: Expecting the multiplication to happen on the way down

In factorial, the product n * smaller is only computed after the call below returns. Going down, nothing is multiplied yet; the results are assembled on the way back up.

Mistake 2: Confusing frames of the same method

Four factorial frames are four separate calls with four separate n values. Select a frame in the debugger to see its own variables.

Mistake 3: Stopping the investigation at the first repeated line

The repeating at ...factorial lines tell you it is recursion without end, not why. Check the base case condition and the argument of the recursive call.

Mistake 4: Leaving trace code behind

The println and Thread.dumpStack() lines used here are scaffolding. Remove them once you understand the behaviour, especially in methods that should only compute (Lesson 2).

Mistake 5: Not restoring the fix

Remember to put the base case back and re-run the original test. A repeated line in the trace means this version is broken; the working version should print 24.

Exercises

Try each exercise yourself before opening the solutions. Where an exercise asks you to run code, a debugger is optional; print statements are enough.

Level A: Foundation

A1. At the deepest point of factorial(3), how many factorial frames are on the stack, and what are their n values from top to bottom?

Show solution A1

A1. Three frames: n = 1 on top, then n = 2, then n = 3, with main below them.

A2. In factorial(4), which frame returns first, and what value does it return?

Show solution A2

A2. The top frame, factorial(1), returns first with the value 1 (base case).

A3. What single repeated line signals an endless recursion in a stack trace?

Show solution A3

A3. The same at Class.method(File.java:line) line, repeated many times in a row.

Level B: Application

B1. Draw a table like Step 1 for sumTo(3) where sumTo(n) returns n + sumTo(n - 1) and sumTo(0) returns 0. List the stack at each event and the final value.

Show solution B1

B1. Stack (top first) at each step: sumTo(3), main; then sumTo(2), sumTo(3), main; sumTo(1), ...; sumTo(0), sumTo(1), sumTo(2), sumTo(3), main (deepest, returns 0). Then sumTo(1) returns 1 + 0 = 1, sumTo(2) returns 2 + 1 = 3, sumTo(3) returns 3 + 3 = 6. Final value 6.

B2. Add enter and leave print lines to sumTo and predict, before running, the exact output of sumTo(3).

Show solution B2

B2. With enter n=X printed on entry and leave n=X result=R before the return in the recursive case, the output is: enter n=3, enter n=2, enter n=1, enter n=0, leave n=1 result=1, leave n=2 result=3, leave n=3 result=6. (n = 0 is the base case and returns without a leave line, if you place that print after the recursive call.)

B3. In a debugger (or by adding prints) examine countdown(3) from Lesson 6. At the base case, how many countdown frames are on the stack?

Show solution B3

B3. Four frames: countdown(0) on top (the base case), then countdown(1), countdown(2), countdown(3), with main at the bottom. Four countdown frames, plus main.

Level C: Challenge

C1. A colleague's method is static int f(int n) { if (n == 0) return 0; return n + f(n - 2); }. Call f(5). Predict what happens, say why, and propose two different fixes.

Show solution C1

C1. f(5) goes 5, 3, 1, -1, -3, ... and never equals 0, so the base case is skipped and the program ends in StackOverflowError. Fix 1: change the base case to if (n <= 0) return 0;, which also stops for odd and negative inputs. Fix 2: change the step so it reaches the base for every input, for example f(n - 1) if that matches the intended meaning, or validate that n is even before calling.

C2. Explain in your own words, using the stack, why the recursive factorial returns 24 for 4 even though no multiplication happens until the base case is reached.

Show solution C2

C2. Each frame has to wait for the frame above it, because n * smaller needs the value of smaller. Only when the base case returns 1 can the frame for n = 2 finish its product, then n = 3, then n = 4. The multiplications are performed from the top of the pile downwards as frames are popped, so the final frame computes 4 * 6 = 24.

Mini Challenge

Trace your own recursive method from this module, either sumOfDigits or your lab factorial, in three ways: (1) a hand-drawn table of stack and variables for one input, (2) enter/leave print statements (and Thread.dumpStack() at the base case), and (3) a debugger session if your tool has one. Then break it by removing the base case, record what the trace looks like, and restore it.

Check your work: the output of (2) matches your table in (1), you can say how many frames exist at the deepest point, and the restored method gives the original correct result with the temporary trace lines removed.

Summary & Next Lesson

Key takeaways

  • A recursive call pushes a new frame with its own variables; frames pile up until the base case is reached.
  • The answers are assembled as frames return, from the top of the pile downwards, in reverse order of the calls.
  • Print statements, Thread.dumpStack() and a debugger's call-stack panel all show the same stack from different angles.
  • An endless recursion shows one repeated line in the trace and ends in StackOverflowError.
  • After a deliberate break, restore the base case and re-test.

Next lesson

This is the last lesson of the module. Take the lab, BEG-014-LAB1 (the command-line calculator), and finish it with a loop-based power and a recursive factorial that has a base case. After that, the course continues with arrays and strings, as listed in the course plan for the next module.

Self-check

  1. How many factorial frames exist at the deepest point of factorial(4)?
  2. Which frame returns first?
  3. When is n * smaller computed: going down or coming back?
  4. What does a trace of one repeated line suggest?
  5. What should you do after demonstrating the missing-base-case bug?
Show answers
  1. Four, with n = 1 on top and n = 4 at the bottom, above main.
  2. The top one, the base case factorial(1).
  3. Coming back, after the call below it has returned its value.
  4. A recursion with no (or an unreachable) base case, ending in StackOverflowError.
  5. Restore the base case and re-run to confirm the correct result.

References

  • Oracle, The Java Virtual Machine Specification, SE 21, Chapter 2. Verified: a frame is created for each method invocation and destroyed when it completes; only the current (top) frame is active; exceeding the allowed stack size throws StackOverflowError.
  • Oracle, StackOverflowError (Java SE 21 API). Verified: thrown when an application recurses too deeply.
  • The enter/leave output, the stack dump and the repeated-line StackOverflowError trace were produced by running the code with JDK 21 (javac 21.0.10). The debugger workflow (breakpoints, call-stack panel) is described from general IDE behaviour and was not run in a debugger here, so button and panel names may differ in your tool.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.