Recursion pitfalls
Most recursion bugs come from a handful of mistakes: a missing or unreachable base case, a wrong return, state shared between calls, recomputed subproblems, too much depth, and overflow. Knowing them lets you find the cause quickly.
Learning goals
- Recognise a missing base case and an unreachable one
- Spot an ignored or wrong return value
- Avoid bugs from shared mutable state
- Prevent StackOverflowError and integer overflow
Prerequisites
Missing base case
With no base case, or a base case the input never reaches, the method calls itself until the stack is exhausted. A frequent variant is checking n == 0 when n can jump over 0 (for example n -= 2 from an odd n).
Use <= or an explicit range check for the base case when the argument can skip values, and write down the shrinking quantity before coding.
1static int countDown(int n) {2if (n == 0) return 0; // bug: countDown(5 - 2 - 2 - 2) skips 0 and goes negative3return countDown(n - 2);4}56static int countDownFixed(int n) {7if (n <= 0) return 0; // fix: any non-positive value stops the recursion8return countDownFixed(n - 2);9}
Incorrect return
A recursive call returns a value, and ignoring it is a common bug: calling sum(n - 1) without using or returning the result computes the right thing and throws it away.
In a method that returns a value from every branch, make sure each recursive path returns. In a search that tries several branches, only return true immediately, and fall through to false after all branches fail.
1static int sumTo(int n) {2if (n == 0) return 0;3int rest = sumTo(n - 1); // computed...4return n; // ...but never used: returns n instead of n + rest5}67static int sumToFixed(int n) {8if (n == 0) return 0;9return n + sumToFixed(n - 1); // the recursive result is used and returned10}
Repeated subproblems
When two branches of the recursion tree solve the same subproblem, the algorithm repeats work and may become exponential. Naive Fibonacci recomputes fib(2) five times for n = 6. The same happens in naive grid-path counting, coin change and edit distance.
The signs are overlapping subproblems and a recursion tree with identical nodes. The fix is to cache results: top-down memoization or bottom-up dynamic programming (Chapter 23).
| Problem | Naive calls | With memoization |
|---|---|---|
| fib(40) | 331,160,281 | 79 |
| Grid paths, 16 × 16 moves | over 600 million | 289 subproblems |
| Climbing 45 stairs (1 or 2 steps) | over 3 billion | about 90 |
StackOverflowError
Java throws StackOverflowError when the thread's call stack is full. The limit is typically a few thousand to tens of thousands of frames, depending on frame size and the -Xss setting. A recursion on a list of 100,000 elements can easily exceed it.
Fixes: make sure there is a base case, reduce depth by halving instead of subtracting, convert to an iterative loop or explicit stack, or run the work in a thread with a bigger stack. Do not catch the error as a normal control path.
1// deep work in a thread with a 256 MB stack2Thread worker = new Thread(null, () -> solve(), "deep", 256L * 1024 * 1024);3worker.start();
Integer overflow
Recursive results can grow quickly: factorial, powers and Fibonacci all exceed int within a few dozen steps. Overflow wraps silently in Java and gives plausible but wrong numbers.
Use long, BigInteger, or modular arithmetic, and use Math.addExact or Math.multiplyExact where you want an exception instead of a silent wrap. Midpoints in divide and conquer should be lo + (hi - lo) / 2.
1int ok = 12 * 11 * 10; // fine2int bad = 13 * 479_001_600; // wraps to 1932053504, not 62270208003long good = 13L * 479_001_600L; // 62270208004long safe = Math.multiplyExact(13L, 479_001_600L); // throws ArithmeticException on overflow
Practice ladder
Now try it yourself
Easy
- Find and fix the missing base case in a given sum method
- Why does factorial(13) print a wrong number with int?
Medium
- Fix a subsets method that adds the same list object to the results
- Measure the deepest recursion your JVM allows for a simple method
Advanced
- Convert a recursive method that overflows the stack to an explicit-stack loop
- Spot the repeated subproblems in a recursive grid path counter and memoize it
Key takeaways
- Check the base case is reachable, the progress is strict, and every path returns the result of the call.
- Copy before storing and undo after recursing when state is shared.
- Memoize repeated subproblems; use long or exact arithmetic; keep recursion depth within the stack limit.