Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely encountered recursion before—it's a clean, elegant way to solve problems that have a naturally repetitive, nested structure. But there is a hidden danger in the way most of us first learn to write recursive functions. If your recursion goes too deep, you'll hit a StackOverflowError, and your application will crash. It's a rite of passage for every developer, but in a production environment, it's a bug we can't afford.

The Stack Overflow Trap

Let's look at a common way to calculate the sum of all numbers from 1 up to n. I'll write this the "naive" way first:

fun sumRange(n: Int): Int {
    if (n <= 0) return 0
    return n + sumRange(n - 1)
}

At first glance, this is perfect. It's readable and mathematically sound. But here is the problem: the computer cannot finish the addition (n + ...) until the recursive call to sumRange(n - 1) returns a value. This means the runtime has to "remember" every single call on a call stack. If you pass in 10, 100, or even 1,000, it works fine. But try passing in 100,000, and the stack will simply run out of space. You've asked the CPU to remember 100,000 pending additions, and it just can't do it.

Moving the Work to the Argument

To fix this, we need to change when the work happens. Instead of waiting for the recursive call to finish before adding, we can pass the current total along as an argument. This is called using an accumulator.

fun sumRangeTail(n: Int, accumulator: Int = 0): Int {
    if (n <= 0) return accumulator
    return sumRangeTail(n - 1, accumulator + n)
}

Notice the difference? The very last thing this function does is call itself. There is no pending addition waiting to happen after the call returns. In theory, the compiler doesn't need to keep the current stack frame alive because there's nothing left to do here once the next call starts. However, in standard JVM bytecode, this is still a recursive call, and you'll still eventually hit that stack limit—just perhaps a bit later.

Letting the Compiler Do the Heavy Lifting

This is where Kotlin's tailrec modifier comes in. By adding this keyword, you're telling the compiler: "I've written this function so that the recursive call is the absolute last operation. Please optimize this for me."

tailrec fun sumRangeOptimized(n: Int, accumulator: Int = 0): Int {
    if (n <= 0) return accumulator
    return sumRangeOptimized(n - 1, accumulator + n)
}

When you add tailrec, the Kotlin compiler actually rewrites your code under the hood. It transforms that recursion into a while loop in the final bytecode. You get the clean, declarative look of recursion in your source code, but the performance and safety of a loop at runtime. Now you can pass 1,000,000 into sumRangeOptimized, and it will return the result instantly without ever touching the stack limit.

Just a heads-up: tailrec only works if the recursive call is the final action. If you try to wrap the call in another operation—like 1 + sumRangeOptimized(...)—the compiler will give you a warning and ignore the modifier, because the "tail" is no longer just the function call. It's an all-or-nothing optimization.




📋 Practical Task

Optimizing a Recursive Factorial Calculator

You have been handed a legacy piece of code that calculates the factorial of a number. It currently uses the naive recursive approach, which causes the system to crash when calculating factorials for large numbers (though the result will overflow a Long eventually, we want to avoid the StackOverflowError first).

Your Task: Rewrite the following function using the accumulator pattern and the tailrec modifier to ensure it can handle deep recursion without crashing the stack.

fun factorial(n: Long): Long {
    if (n <= 1) return 1
    return n * factorial(n - 1)
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.