🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
Exception in thread "main" java.lang.StackOverflowError
at com.devinhyderabad.recursion.Fibonacci.fib(Fibonacci.java:6)
at com.devinhyderabad.recursion.Fibonacci.fib(Fibonacci.java:6)
at com.devinhyderabad.recursion.Fibonacci.fib(Fibonacci.java:6)
// ... the same frame repeats up to the print cap: MaxJavaStackTraceDepth=1024
// by default, then output simply stops. No message text exists, no Caused by.
// Mutual recursion alternates two or more distinct frames instead.⚡ Quick Fix Works 80% of the time
Open the file named in the TOP repeated frame and add the missing termination condition — or replace the recursion with iteration.
// Before: no guard for unexpected inputs -> infinite descent
static long fib(int n) { return fib(n - 1) + fib(n - 2); }
// After: provable base case
static long fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}🧠 Why this Happens
Tap to expand the deep technical explanation
Each method invocation pushes a frame holding locals, operands, and bookkeeping onto the current thread's fixed-size stack (-Xss). HotSpot maps guard pages at the stack end; when a push touches the guard, the OS signal handler converts it into StackOverflowError — with no message because the condition is positional (no room left), not data-dependent, so there is nothing meaningful to describe. Being an Error rather than Exception, ordinary catch(Exception) blocks let it tear down thousands of frames at once. The famous 1024-line wall in logs is cosmetic: MaxJavaStackTraceDepth limits printing, while the true depth was whatever fit in the stack.
The HITEC City Parking Spot Analogy:
A lift that only records upward trips: recursion keeps pressing "up". The alarm fires at the shaft ceiling regardless of which button started the ride — which is why the log repeats the same floor hundreds of times and then goes silent.
🔁 How to Reproduce Confirm this is your error
Write static long fib(int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); } and call fib(100_000) — overflow trips within milliseconds; no waiting involved anywhere. Canonical log format shown; header line identical on JDK 8 through 25 (not lab-captured: wording carries no version split).
🛠️ Solutions (5 Ways to Fix)
Chase the top frame and add a provable base case
👉 Use this first — the deepest repeated frame names the method stuck calling itself.
Open the file at the top frame line and ask what should have stopped the calls: a missing base case, a parameter moving the WRONG way (counting up instead of down), or state mutated before the recursive step. Mutual recursion shows as an alternating pair of frames — the cycle spans two methods, so the guard may belong in either.
// Broken: n grows forever on the recursive branch
static int walk(int n) { return walk(n + 1); }
// Fixed: parameter moves toward the base case
static int walk(int n) {
if (n >= MAX_DEPTH) return n;
return walk(n + 1);
}Break implicit cycles in toString, equals, and JSON
👉 Use this when no obvious manual recursion exists but SOE hits during logging or serialization.
Bidirectional relationships recurse without any explicit self-call: Lombok @Data generates equals/hashCode/toString traversing BOTH directions of parent<->child, and Jackson serializes the same loop. Exclude the back-reference on the child side — one annotation ends the cycle for logging AND mapping.
@Data
public class Parent {
private List<Child> children;
}
@Data
public class Child {
@ToString.Exclude
@EqualsAndHashCode.Exclude
@JsonIgnore // Jackson equivalent
private Parent parent;
}Convert deep linear recursion into iteration
👉 Use this when input depth is user-controlled (tree depth, nested JSON, linked lists) and cannot be bounded.
Every recursion level costs a real stack frame sized by -Xss; an explicit Deque moves the same work onto the heap, which grows dynamically. Depth-first traversal becomes a while-pop loop; tail-recursive accumulation becomes a simple loop variable.
// Recursive DFS -> heap-backed stack
Deque<File> pending = new ArrayDeque<>();
pending.push(root);
while (!pending.isEmpty()) {
File f = pending.pop();
Arrays.stream(f.listFiles()).forEach(pending::push);
process(f);
}Size stacks deliberately — and know what raising them costs
👉 Use this only when recursion depth is legitimately large AND bounded (parser chains, expression trees).
-Xss trades address space for depth: doubling it doubles survivable depth but also multiplies per-thread reservation across every thread pool — 200 threads × bigger stacks is real memory. Raise it as a measured last resort AFTER confirming depth is legitimately bounded; otherwise you are paying RAM to postpone the same crash deeper.
# Legitimate bounded-depth case:
java -Xss2m -jar app.jar # ~4x default depth for parser-heavy workloads
# Verify current setting on a running process:
jcmd <pid> VM.flags | grep -o ThreadStackSize=[0-9]*Diagnose swallowed SOEs and regex mislabels
👉 Use this when the error vanishes mysteriously or a regex gets blamed for a hang.
catch (Exception e) does NOT catch StackOverflowError (it is an Error) — catching Throwable/Error leaves the app in a half-unwound state; log-and-rethrow instead. Conversely, catastrophic regex backtracking usually manifests as a TIMEOUT, not SOE: a jstack dump shows the thread parked inside java.util.regex.Pattern$... for minutes rather than climbing application frames.
# Is it really a regex problem? Parked vs climbing frames:
jstack <pid> | grep -A6 "http-nio.*exec"
# SOE in progress: repeating YOUR frames
# Backtracking: one frame inside java.util.regex.Pattern$Loop/.match📋 Version Notes
Default thread stack sizes platform-dependent (512 KB–1 MB); the Error itself message-less and unchanged.
MaxJavaStackTraceDepth=1024 caps PRINTED frames (the log truncates, not the actual depth); unchanged semantics.
No behavioral change; Lombok/Jackson cycle annotations remain the practical fix for framework-induced overflow.
Virtual threads schedule onto carrier threads with their own stacks — unbounded USER recursion still overflows identically.
🛡️ How to Prevent This Next Time
Give every recursion a reviewed, provable base case; annotate bidirectional entity relations against accidental toString/JSON traversal; load-test worst-case input depths on representative -Xss settings; and monitor stack depth in tests via Thread.currentThread().getStackTrace().length for regression alarms.