Careeroza
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...")
}