🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOGException 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)

Solution 1✓ Most common cause

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); }
Solution 2

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; }
Solution 3

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); }
Solution 4

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]*
Solution 5

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

Java 8

Default thread stack sizes platform-dependent (512 KB–1 MB); the Error itself message-less and unchanged.

Java 11

MaxJavaStackTraceDepth=1024 caps PRINTED frames (the log truncates, not the actual depth); unchanged semantics.

Java 17

No behavioral change; Lombok/Jackson cycle annotations remain the practical fix for framework-induced overflow.

Java 21

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.