Stack Overflow in mobile development — what it is, causes and prevention methods

Author: IT Sectr Published: 2026-03-29 Reading time: 9 min

Stack Overflow is a call stack overflow error (java.lang.StackOverflowError) that occurs when the maximum stack depth of a thread is exceeded. According to the Java Virtual Machine Specification, the typical JVM stack depth is 1024 frames for 64-bit systems. The main cause is infinite recursion without a base case.

Key Takeaways

  • StackOverflowError — a JVM error when the call stack depth limit is exceeded
  • Stack depth is limited to 512–2048 frames depending on configuration
  • Infinite recursion is the most common cause of StackOverflowError
  • Tail recursion is not optimized in JVM, unlike functional languages
  • Iterative replacement of recursion is a reliable way to prevent overflow

What is Stack Overflow

StackOverflowError is a fatal error of the Java Virtual Machine (JVM) or Android Runtime (ART) that occurs when the call stack of a thread reaches the maximum allowed depth. Unlike OutOfMemoryError (Heap exhaustion), StackOverflowError is related to a different memory area — the stack, where method call frames and local variables are stored.

Each method call creates a frame on the stack: return address, parameters, and local variables. When the method returns, the frame is destroyed. If a method calls itself (recursion) without a base case, frames accumulate until the stack is full. The JVM cannot allocate a new frame and throws StackOverflowError with a «null» message (in Java) or with an indication of an endlessly repeating stack trace line.

The size of a thread's stack is fixed at creation and does not change during execution. In Android, the typical main thread stack size is 32–48 KB, which gives a depth of roughly 512–1024 frames for methods without many local variables. For background threads, the default size is smaller — 16–24 KB.

How the Call Stack Works

The Call Stack is a LIFO (Last In, First Out) data structure that manages the order of method execution. Each time the program calls a method, the JVM creates a frame on the stack and places it on top. When the method completes, the frame is popped.

Each frame contains: operand stack (for bytecode instructions), array of local variables (including this), reference to constant pool, and return address. The more local variables a method has, the larger its frame size and the fewer methods can be called before the stack is full. A method with 10 parameters and 20 local variables takes about 3 times more space than a method with no parameters.

On Android, ART uses its own stack implementation, different from Desktop JVM. ART can dynamically increase the stack within certain limits, but a hard limit still exists for each thread. The main thread (UI thread) has the largest stack, as it handles the entire Activity lifecycle and event processing.

kotlin
// Recursion that leads to StackOverflowError
fun recursiveCall(depth: Int): Int {
    return recursiveCall(depth + 1) // no base case
}

// The call will cause StackOverflowError at depth ~1000
recursiveCall(0)

Main Causes of Stack Overflow

Five typical scenarios lead to StackOverflowError in mobile applications. Most of them are related to recursion, but there are also less obvious causes.

Infinite Recursion Without a Base Case

The most common cause. A developer writes a recursive method without a stopping condition or with a condition that never becomes true. Each call adds a frame, and the stack fills up in 500–2000 iterations depending on frame size. A typical example: calculating n! factorial without checking n == 0.

Check the base case at the beginning of each recursive method. In Kotlin, use require() or check() for parameter validation at the start. For deep recursion (more than 100 levels), consider replacing it with an iterative approach.

Cyclic Dependencies in Constructors

Class A creates instance B, class B creates instance A — this is a cyclic dependency in constructors. When trying to create A, the B constructor is called, which calls the A constructor, and so on until StackOverflowError. DI frameworks (Dagger, Hilt) detect such cycles at compile time, but manual object creation does not catch them.

Use Dependency Injection with dependency graphs: Dagger or Koin check cycles at build time. If a cycle is unavoidable, replace direct dependency with an interface using lazy initialization or a Provider factory.

kotlin
// Cyclic dependency — StackOverflowError
class A(private val b: B)
class B(private val a: A)

// Lazy solution
class A(private val bProvider: Provider<B>)

Deep Recursion in Graph Traversal

Traversing a View tree (ViewGroup.getChildAt()), file system, or JSON structure via recursion may exceed the stack limit at a depth of more than 500–1000 elements. An Android ViewGroup with 20 levels of nesting is rare, but recursive parsing of JSON with 2000 nested objects is a real scenario.

Replace recursive traversal with iterative traversal using an explicit Stack<T> or ArrayDeque. This completely eliminates the risk of stack overflow, since heap objects are not limited by the stack limit. BFS (Breadth-First Search) via Queue also solves the problem.

Improper Handling of onConfigurationChanged

An Android-specific cause: cyclic calls of lifecycle methods when configuration is handled incorrectly. For example, calling recreate() inside onConfigurationChanged, which again calls onConfigurationChanged, and so on until StackOverflowError. Similarly: setContentView() inside onLayout(), which triggers another measure and layout pass.

Do not call recreate() inside methods related to configuration changes. To update the UI when the theme changes, use setTheme() without recreate. For dynamic orientation changes — call requestOrientation() once, without a flag in the configuration.

Serialization with Cyclic References

Gson, Moshi, or Kotlin Serialization when trying to serialize an object with cyclic references (A references B, B references A) go into infinite recursion and crash with StackOverflowError. This is a common problem when serializing entities with bidirectional relationships (JPA, Room with ForeignKey).

Use @Transient, @JsonIgnore, or @kotlinx.serialization.Transient for one side of the cycle. For Gson — JsonSerializer with an explicit depth limit. For Room — never serialize Entity directly, use DTO mappers.

How to Diagnose and Fix StackOverflowError

Diagnosing StackOverflowError is easier than other memory errors: the stack trace in most cases shows a repeating sequence of calls. This immediately points to recursion.

Reading the Stack Trace

The Stack trace of StackOverflowError is unique: after the first 200–500 lines, the same call pattern starts repeating. The JVM truncates repeating lines at the end and shows «... 1234 more». The number of non-repeating lines before «...» indicates the recursion depth that caused the error.

Read the first lines of the stack trace — they show which method started the repetition. Find the method that calls itself or creates a call chain that returns to it. Fix the base case or replace recursion with a loop.

Increasing Stack Size (Temporary Solution)

Temporarily, the problem can be solved by increasing the stack size via the JVM flag -Xss. For Android, the stack size is set via AndroidManifest: android:largeHeap does not affect the stack. To increase the thread stack in code: Thread(ThreadGroup, Runnable, name, stackSize). stackSize is the desired size in bytes.

kotlin
// Creating a thread with an increased stack
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()

Important: increasing the stack does not solve the problem, it only delays it. With recursion of 10,000 levels, a 64 KB stack will be replaced by a 128 KB stack, giving 20,000 levels — but the error will still occur, just later. The only correct solution is iterative replacement of recursion.

Replacing Recursion with Iteration

Iterative algorithms do not use the call stack to store intermediate states — they store them in the heap (Stack<T> or ArrayDeque). Binary tree traversal, factorial calculation, Fibonacci — any recursion can be converted to iteration using an explicit stack.

kotlin
// Iterative tree traversal — no risk of StackOverflow
fun traverseIterative(root: Node?) {
    val stack = ArrayDeque<Node>()
    stack.push(root)
    while (stack.isNotEmpty()) {
        val node = stack.pop() ?: continue
        process(node)
        node.right?.let { stack.push(it) }
        node.left?.let { stack.push(it) }
    }
}

How to Prevent Stack Overflow

Preventing StackOverflowError is a set of rules and tools that identify potential recursive cycles before they reach production.

Recursion Depth Limit in Debug Build

Add a protective depth counter in recursive methods in debug builds. If the depth exceeds a threshold (e.g., 1000), throw an exception with a clear message. This turns a StackOverflowError with an unreadable trace into a clear business exception.

kotlin
fun safeRecursive(n: Int, depth: Int = 0): Int {
    if (depth > 1000) {
        throw IllegalStateException("Recursion exceeded 1000 levels")
    }
    return if (n <= 1) n
           else safeRecursive(n - 1, depth + 1)
}

Static Code Analysis

Detekt (Kotlin) and Infer (Facebook) find potential infinite recursions at the static analysis level. Detekt has a PotentiallyInfiniteRecursion rule that warns about self-calls without changing parameters. Enable it in the CI ruleset and set severity to error.

Code Review with Focus on Recursion

During code review, pay attention to: any self-call methods, recursive calls inside lambdas (Kotlin inline functions), cyclic calls between different classes, recursion in property delegates. For each recursive method, check: is there a base case, does the parameter change at each step, does the parameter change guarantee reaching the base case.

Tail-Recursive Transformation (Limited)

Kotlin supports the tailrec modifier: if a recursive method is marked tailrec and the call is tail (last operation), the compiler converts it to iteration. However, tailrec only works for self-calls (method calls itself directly), does not work for mutual recursion, and is not supported in Android-compatible Kotlin versions before 1.5.

kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc
           else factorial(n - 1, acc * n) // tail call
}

Frequently Asked Questions

Can StackOverflowError be caught with try-catch?

Yes, but only at the Java level. Error, like Exception, is a Throwable. However, after StackOverflowError the stack is damaged — frames that did not fit cannot complete correctly. An attempt to create a new object in the catch block may cause another StackOverflowError.

What is the default stack size in Android?

For the main thread — 32–48 KB, for background threads — 16–24 KB. The exact size depends on the Android version and device manufacturer. ART uses dynamic stack expansion but no more than 2× the initial value.

Can tail recursion prevent StackOverflowError?

In Kotlin — yes, if the method is marked tailrec. The compiler converts tail recursion into iteration, completely eliminating stack growth. In Java, tail recursion is not optimized by JVM (unlike functional languages like Scala).

Why does StackOverflowError occur on the emulator but not on the device?

The stack size on the emulator and a real device may differ. The emulator uses Desktop JVM with a typical stack of 512–1024 KB, while Android ART uses 32–48 KB. The error will manifest on ART earlier than on Desktop JVM.

How is StackOverflowError different from OutOfMemoryError?

Memory area: StackOverflowError is a stack error (call frames), OutOfMemoryError is a heap error (objects). StackOverflowError is almost always caused by recursion, while OutOfMemoryError is caused by memory leaks or large objects.

Summary

  • StackOverflowError — call stack overflow when the recursion depth limit is exceeded
  • Stack depth in Android is 512–1024 frames on the main thread
  • Infinite recursion is the main cause; check the base case in every recursive method
  • Cyclic dependencies in constructors — a less obvious but common cause of overflow
  • Iterative replacement of recursion via explicit Stack<T> completely eliminates the risk
  • tailrec in Kotlin converts tail recursion to iteration at the compiler level
  • Static analysis (Detekt, Infer) finds potentially infinite recursions before runtime

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also