Diagnosing and Resolving StackOverflowError in Scala Recursive Functions
A guide to diagnosing and fixing StackOverflowError in Scala. Learn how to use @tailrec, implement accumulators, and use trampolining to move recursive calls from the stack to the heap.
21 Apr 2026, 02:12 UTC

The Problem: Stack Exhaustion in Deep Recursion
A java.lang.StackOverflowError occurs when a program pushes more frames onto the call stack than the JVM's memory limit allows. In Scala, this most commonly happens when processing large datasets—such as deep trees or long lists—using recursive functions that the compiler cannot optimize into loops.
The critical takeaway is that recursion is not free. Unless a function is specifically designed for tail-call optimization (TCO), every recursive call consumes a new stack frame. Once the limit (defined by the -Xss JVM flag) is reached, the application crashes regardless of how much heap memory is available.
Diagnostic Matrix
Use this table to identify the cause of your crash based on the observed behavior.
| Symptom | Likely Cause | Diagnostic Signal |
|---|---|---|
| Crash only with large inputs | Non-tail-recursive function | Stack trace shows repeating patterns of the same method call |
| Immediate crash on start | Mutual recursion (A calls B, B calls A) | Stack trace toggles between two different method signatures |
| Crash after increasing -Xss | Algorithmic complexity issue | Error persists despite larger stack allocation |
Step-by-Step Resolution Path
Step 1: Verify Tail-Call Eligibility
The first step is to determine if the Scala compiler can optimize your function. A function is tail-recursive only if the recursive call is the absolute final action performed by the method.
Apply the @tailrec annotation to the suspected method. This annotation does not change the code's behavior; it instructs the compiler to emit an error if the method cannot be optimized into a loop.
import scala.annotation.tailrec
// This will fail to compile because the addition happens AFTER the call
@tailrec
def sum(list: List[Int]): Int = list match {
case Nil => 0
case head :: tail => head + sum(tail)
}
Expected Result: If the compiler throws "method cannot be made tail-recursive", you have confirmed the cause of the StackOverflowError.
Step 2: Implement an Accumulator
To fix a non-tail-recursive function, move the pending computation into a parameter called an accumulator. This ensures the recursive call is the final operation.
import scala.annotation.tailrec
def sum(list: List[Int]): Int = {
@tailrec
def loop(remaining: List[Int], acc: Int): Int = remaining match {
case Nil => acc
case head :: tail => loop(tail, acc + head)
}
loop(list, 0)
}
Verification: Re-run the code with the dataset that previously triggered the crash. The @tailrec annotation now confirms the compiler has converted this into a while loop internally.
Step 3: Resolve Mutual Recursion via Trampolining
Standard TCO cannot optimize mutual recursion (where Function A calls Function B, which calls Function A). In these cases, use scala.util.control.TailCalls to move the execution from the JVM stack to the heap.
Configuration Example:
import scala.util.control.TailCalls._
def isEven(n: Int): TailRec[Boolean] =
if (n == 0) done(true) else tailcall(isOdd(n - 1))
def isOdd(n: Int): TailRec[Boolean] =
if (n == 0) done(false) else tailcall(isEven(n - 1))
// To execute, call .result on the final TailRec object
val result = isEven(100000).result
Risk: Trampolining introduces a slight performance overhead due to the creation of TailRec objects on the heap, but it prevents stack exhaustion for arbitrary depths.
Comparison of Fixes
| Method | Complexity | Performance | Best Use Case |
|---|---|---|---|
| Accumulator | Low | Highest | Simple linear recursion |
| Trampolining | Medium | Moderate | Mutual or complex recursion |
| Increasing -Xss | Trivial | High | Legacy code where logic cannot be changed |
Escalation Criteria
If the following conditions are met, the issue may be a memory leak or a logic loop rather than a recursion depth problem:
- The
StackOverflowErroroccurs with very small input sizes (e.g., a list of 10 elements). - The stack trace shows an infinite loop between two methods without any base-case progression.
- Increasing
-Xss(e.g.,-Xss4m) provides no increase in the maximum processable input size.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.