Retry and Result Patterns

Retry with Backoff

Example

A reusable retry helper with exponential backoff. It never retries cancellation.

import kotlinx.coroutines.*
import java.io.IOException

suspend fun <T> retry(
    times: Int,
    initialDelay: Long = 100,
    factor: Double = 2.0,
    block: suspend (attempt: Int) -> T,
): T {
    var wait = initialDelay
    repeat(times - 1) { attempt ->
        try {
            return block(attempt + 1)
        } catch (e: IOException) {
            println("Attempt ${attempt + 1} failed: ${e.message}; retrying in $wait ms")
        }
        delay(wait)
        wait = (wait * factor).toLong()
    }
    return block(times)          // last attempt: let it throw
}

var calls = 0
suspend fun fetchBalance(): Int {
    calls++
    delay(20)
    if (calls < 3) throw IOException("timeout")
    return 2500
}

fun main() = runBlocking {
    val balance = retry(times = 4) { fetchBalance() }
    println("Balance: Rs $balance after $calls calls")
}

Output

Attempt 1 failed: timeout; retrying in 100 ms
Attempt 2 failed: timeout; retrying in 200 ms
Balance: Rs 2500 after 3 calls

Returning Result Instead of Throwing

Example

Many apps convert exceptions to a sealed result type at the repository boundary, so the UI just renders states.

import kotlinx.coroutines.*
import java.io.IOException

sealed interface LoadResult<out T> {
    data class Success<T>(val data: T) : LoadResult<T>
    data class Error(val message: String) : LoadResult<Nothing>
}

suspend fun <T> safeCall(block: suspend () -> T): LoadResult<T> = try {
    LoadResult.Success(block())
} catch (e: CancellationException) {
    throw e
} catch (e: IOException) {
    LoadResult.Error("No internet: ${e.message}")
}

fun main() = runBlocking {
    val ok = safeCall { delay(10); listOf("Order #1", "Order #2") }
    val bad = safeCall<List<String>> { throw IOException("host unreachable") }
    for (r in listOf(ok, bad)) {
        when (r) {
            is LoadResult.Success -> println("Show list: ${r.data}")
            is LoadResult.Error -> println("Show error: ${r.message}")
        }
    }
}

Output

Show list: [Order #1, Order #2]
Show error: No internet: host unreachable

Exercise: Change retry so it also gives up early (throws at once) when the exception is an IllegalArgumentException, because invalid input will never succeed on retry.

Show answer
// Only IOException is caught, so any other exception (including
// IllegalArgumentException) already escapes on the first attempt.
// To make it explicit:
try {
    return block(attempt + 1)
} catch (e: IllegalArgumentException) {
    throw e                        // do not retry bad input
} catch (e: IOException) {
    println("retrying...")
}