The Error
You run your Java application and the JVM throws:
Exception in thread "main" java.lang.StackOverflowError
at com.example.MyClass.calculate(MyClass.java:12)
at com.example.MyClass.calculate(MyClass.java:12)
at com.example.MyClass.calculate(MyClass.java:12)
... (repeated hundreds of times)
Same line, repeated hundreds of times. That's the tell โ the JVM exhausted its call stack space before your code could finish.
Why This Happens
Each method call in Java gets its own stack frame. The JVM reserves a fixed chunk of memory for this per thread โ typically 512KB to 1MB. If calls nest too deeply without unwinding, that space fills up fast.
Three scenarios cause this most often:
- Missing or wrong base case in a recursive method
- Mutual recursion โ method A calls method B, which calls method A
- Accidental self-calls โ a getter calling itself, or a
toString()that triggers anothertoString()
Fix 1 โ Correct the Base Case
Find the repeated line in your stack trace, open that method, and check whether it has a stopping condition.
Broken:
public int factorial(int n) {
return n * factorial(n - 1); // No base case โ runs forever
}
Fixed:
public int factorial(int n) {
if (n stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.println(node.val);
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
}
Fix 3 โ Hunt Down Accidental Self-Calls
This one catches people off guard. A toString() that references itself instead of a field will spin forever:
// Bug: toString calls itself
@Override
public String toString() {
return "User: " + toString(); // Should be this.name, not the method
}
// Fix:
@Override
public String toString() {
return "User: " + this.name;
}
Also watch for Lombok or IDE-generated equals()/hashCode() on entities with circular references โ this is common in bidirectional JPA relationships. Annotate the back-reference field with @ToString.Exclude or @EqualsAndHashCode.Exclude to break the cycle.
Fix 4 โ Bump the Stack Size (Last Resort)
Can't refactor right now? You can buy time with the -Xss flag. The default is usually 512k or 1m โ try quadrupling it:
java -Xss4m -jar your-app.jar
Need a larger stack for one specific thread? Set it in the Thread constructor:
Thread thread = new Thread(null, () -> {
runDeepRecursion();
}, "deep-thread", 4 * 1024 * 1024); // 4MB stack
thread.start();
This doesn't fix anything. With large enough input, the error will come back. Use this while you work on the real fix โ not instead of it.
Verification Steps
Once you've applied a fix, run through these checks:
- Re-run the exact input that crashed the app. The error should be gone.
- Test edge cases:
n = 0,n = 1, negative values, empty lists. - Throw a large input at your iterative version โ try
n = 100000โ to confirm it holds up. - Lock it in with a unit test so it can't regress:
@Test
public void testFactorialLargeInput() {
// Should not throw StackOverflowError
assertDoesNotThrow(() -> factorial(10000));
}
Prevention Tips
- Write the base case first, at the top of the method, before any recursive call.
- For user-controlled input, cap recursion depth explicitly:
public void traverse(Node node, int depth) {
if (node == null || depth > 1000) return;
traverse(node.next, depth + 1);
}
- Graphs and trees can have cycles. Track visited nodes in a
Setto avoid looping forever. - In code review, treat every recursive method as a risk โ ask whether the base case is correct and whether it can actually be reached.

