Tutorials Logic, IN info@tutorialslogic.com

StackOverflowError in Java: Causes and Fixes

What is This Error?

StackOverflowError means one thread exhausted the memory reserved for nested method-call frames.

The repeating frames near the top of the stack trace usually reveal infinite recursion, mutual recursion, or an unexpectedly deep input.

Fix the termination or depth model first; increasing -Xss only changes the limit and can reduce how many threads a process can support.

Each Java thread has a call stack. A method invocation adds a frame containing return state and local execution data; returning removes that frame. StackOverflowError is thrown when another frame cannot be created. It is an Error rather than a normal recoverable application exception because execution has reached a JVM resource boundary.

Recursion is not automatically faulty. A recursive method is safe only when every path has a reachable base case and the maximum depth fits supported input. A balanced tree may have logarithmic depth, while a degenerate tree with the same node count can produce one frame per node.

Common Causes

  • Recursive method without a base case
  • Base case that is never reached
  • Mutual recursion (method A calls B, B calls A)
  • Deeply nested data structures (very deep trees)
  • toString() calling itself indirectly

Quick Fix (TL;DR)

Immediate Fix: Missing a stopping condition

Immediate Fix: Missing a stopping condition
// Missing a stopping condition.
int brokenFactorial(int n) {
    return n * brokenFactorial(n - 1);
}

// The recursive argument moves toward a reachable base case.
int factorial(int n) {
    if (n < 0) throw new IllegalArgumentException("n must be non-negative");
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

long factorialIterative(int n) {
    if (n < 0) throw new IllegalArgumentException("n must be non-negative");
    long result = 1;
    for (int i = 2; i <= n; i++) result *= i;
    return result;
}
Output
factorial(5) and factorialIterative(5) both return 120; brokenFactorial eventually throws StackOverflowError.

A valid base case is only half of the proof: the recursive argument must move toward it. The iterative version removes call-depth growth but still has numeric-overflow limits.

Common Scenarios & Solutions

Failure: No base case infinite recursion

Failure: No base case infinite recursion
int sum(int n) {
    return n + sum(n - 1); // Wrong No base case "-- infinite recursion!
}
Output
Calling sum(1) eventually throws StackOverflowError.

The argument decreases forever because zero has no return path.

Correction: Base case

Correction: Base case
int sum(int n) {
    if (n <= 0) return 0; // Correct Base case
    return n + sum(n - 1);
}
Output
sum(3) returns 6.

The base case handles zero and negative input before the next recursive call.

Failure: Unsafe: calls next.toString(), infinite if circular

Failure: Unsafe: calls next.toString(), infinite if circular
class Node {
    Node next;
    @Override
    public String toString() {
        return "Node{next=" + next + "}"; // Unsafe: calls next.toString(), infinite if circular.
    }
}
Output
A cyclic next link repeatedly formats the same nodes until the stack is exhausted.

Following next recursively is unsafe when a linked structure can contain a cycle.

Correction: Don't recurse into next just show value

Correction: Don't recurse into next just show value
class Node {
    int value;
    Node next;
    @Override
    public String toString() {
        // Correct Don't recurse into next "-- just show value
        return "Node{value=" + value + ", hasNext=" + (next != null) + "}";
    }
}
Output
Example: Node{value=7, hasNext=true}.

Printing only local state prevents toString from traversing an unbounded graph.

Correction: Recursive Fibonacci (can overflow for large n)

Correction: Recursive Fibonacci (can overflow for large n)
// Recursive Fibonacci (can overflow for large n)
int fibRecursive(int n) {
    if (n <= 1) return n;
    return fibRecursive(n - 1) + fibRecursive(n - 2);
}

// Correct Iterative Fibonacci (no stack overflow)
long fibIterative(int n) {
    if (n <= 1) return n;
    long a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        long temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

// Correct Or use explicit stack for tree traversal
void traverseIterative(TreeNode root) {
    Deque<TreeNode> stack = new ArrayDeque<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        System.out.println(node.value);
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
}
Output
The iterative traversal prints each reachable node once when the input is an acyclic tree.

Iteration stores pending state explicitly and avoids one JVM call frame per input level.

Best Practices to Avoid This Error

  • Always define a base case - Every recursive method needs a termination condition
  • Verify the base case is reachable - Trace through your logic manually
  • Prefer iteration over recursion - For large inputs, iterative solutions are safer
  • Use memoization - Cache results to avoid redundant recursive calls
  • Use explicit stack for deep traversals - Replace recursion with a Stack/Deque
  • Increase JVM stack size if needed - Use -Xss flag (e.g., -Xss4m)
  • Be careful with toString() in linked structures - Avoid recursive printing

Replace Unbounded Recursion with Explicit State

Increasing the thread stack can postpone StackOverflowError, but it does not correct an algorithm whose depth grows without a safe bound. First prove that every recursive branch approaches a base case. Then estimate the maximum depth from the input. A recursive tree traversal whose depth equals the height may be reasonable for a balanced tree and unsafe for a degenerate one.

When depth can grow with untrusted or production-sized input, move pending work into a heap-backed collection. An explicit stack preserves depth-first order while making memory use visible and controllable. A queue provides breadth-first traversal when level order is acceptable. Both approaches also provide a natural place to enforce node, depth, or time limits.

Do not catch StackOverflowError and continue ordinary processing. The thread may have too little stack space for reliable recovery logic. Treat it as evidence that recursion bounds, cyclic data, or termination logic must be corrected, then restart the operation from a known state.

  • Track visited identity when object graphs may contain cycles.
  • Test the smallest input, the largest supported depth, and a deliberately cyclic structure.
  • Measure explicit work-list size so resource limits can fail predictably before process stability is threatened.

Iterative Depth-First Traversal

Iterative Depth-First Traversal
import java.util.ArrayDeque;
import java.util.Deque;

public class Main {
    public static void main(String[] args) {
        Deque<Integer> pending = new ArrayDeque<>();
        pending.push(3);
        while (!pending.isEmpty()) {
            int value = pending.pop();
            System.out.print(value + " " );
            if (value > 0) pending.push(value - 1);
        }
    }
}
Output
3 2 1 0 

The deque stores pending work on the heap, so algorithm depth is no longer tied to one Java thread stack.

Read the Repeating Stack Trace

Start at the first application-owned frame and look for a repeating method or a short cycle of methods. A direct recursion trace repeats one method and line. Mutual recursion alternates methods such as parseExpression and parseGroup. Framework frames may surround the cycle, so identify the earliest point where application data or control re-enters the same path.

Preserve the complete trace and the input that triggered it. Logging only the error message discards the frames needed for diagnosis. If the trace is shortened by tooling, reproduce with an uncapped trace in a safe environment. Add input size, tree depth, request identity, and relevant configuration so a finite-but-excessive depth can be distinguished from a logic loop.

  • Check recursive getters, equals, hashCode, and toString methods on cyclic object graphs.
  • Inspect serialization mappings where parent and child objects refer to each other.
  • Review proxy, listener, and interceptor code that may call the intercepted operation again.
  • Use a debugger breakpoint or depth counter to prove which state stops changing.

Choose the Correct Fix

For infinite recursion, repair the control flow: add the missing base case, make progress toward it, or break a cycle with visited-object identity. For valid but input-dependent depth, replace recursion with an explicit Deque, enforce a supported depth limit, rebalance the data structure, or process it in chunks. Tail-recursive Java code still consumes frames because the Java language and JVM do not guarantee tail-call elimination.

The -Xss option changes the stack reservation per thread. A larger value may be justified for a measured, finite algorithm that cannot reasonably be rewritten, but it increases per-thread memory pressure and can reduce concurrency. Record the workload, recursion depth, JVM, architecture, and thread count when testing it. Never present a larger stack as the correction for non-terminating recursion.

Do not routinely catch StackOverflowError and continue the same operation. Even if a narrow boundary records the failure, the request state may be incomplete and recovery code itself needs stack. Correct the root cause, reject unsupported input predictably, and restart work from a known state.

  • Test empty input, one element, the largest supported depth, and deliberately cyclic input.
  • Add a depth or node budget at trust boundaries when input controls traversal shape.
  • Measure the iterative work-list size as well as call-stack removal.
  • Verify output equivalence before replacing a recursive traversal with an iterative one.
Before you move on

StackOverflowError in Java: Causes and Fixes Mastery Check

4 checks
  • I can identify the repeating call frames and the missing termination condition in a stack trace.
  • I can distinguish infinite recursion from a finite input that is simply too deep for the thread stack.
  • I can add a boundary test that proves recursive calls move toward a base case.
  • I can decide when iteration or an explicit work stack is safer than increasing -Xss.

Try this next

Core Java Stack Overflow Error Repair Drills

0 of 2 completed

  1. Create a three-node cycle, identify the repeating frames in the stack trace, and add an identity-based visited set that visits each node once. A base case based only on null does not terminate a cycle.
  2. Rewrite a recursive linked-list count as a loop and compare both implementations on a list large enough to exhaust the recursive version. Preserve the same result while moving pending work from call frames into local state.

Core Java Questions Learners Ask

It's caused by infinite recursion "" a method calling itself without a proper base case, or a base case that's never reached. Each method call adds a frame to the call stack, which eventually runs out of space.

Add or fix the base case in your recursive method. Verify the recursion actually moves toward the base case. Consider converting to an iterative solution for large inputs.

Yes, use the JVM flag -Xss: java -Xss4m MyProgram. But this is a workaround "" fixing the recursion logic is the proper solution.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.