Kotlin Flow provides powerful tools for dealing with asynchronous streams of data. One common challenge is what to do when something goes wrong — network failures, timeouts, unexpected exceptions. That's where retry operators come in.
In this post, we'll explore:
- What
retryandretryWhendo - How they differ
- How to use them safely
- Implementing exponential backoff with retries
What Are Retry Operators?
retry: A convenience operator that attempts to re-collect a flow when an error occurs, based on a predicate and given retry count.retryWhen: A more flexible variant that lets you decide per attempt whether to retry, with information about the cause and attempt number.
They let your flows handle transient failures without bubbling up errors immediately.
retryWhen in Detail
Signature (simplified):
fun <T> Flow<T>.retryWhen(
predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean
): Flow<T>- cause: The exception thrown.
- attempt: A zero-based counter of how many retries have already occurred.
You return true when you want to retry, false otherwise. Because it's suspending, you can delay between retries (or do other asynchronous work).
Example:
flowOfData()
.retryWhen { cause, attempt ->
if (cause is IOException && attempt < 3) {
delay(2000) // wait before retrying
true
} else {
false
}
}
.catch { error ->
// Handle final failure here
}
.collect { value ->
// Handle successful emissions
}retry in Detail
Signature (simplified):
fun <T> Flow<T>.retry(
retries: Long = Long.MAX_VALUE,
predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T>Under the hood, retry uses retryWhen, wrapping around it. It's good when you don't need to inspect attempt count, or want a fixed retry limit.
Simple examples:
// Retry indefinitely (until success or flow completes without throwing)
flowOfData()
.retry()
.collect { /* … */ }
// Retry up to 2 times
flowOfData()
.retry(retries = 2)
.collect { /* … */ }
// Retry when IOException only
flowOfData()
.retry(retries = 3) { cause ->
cause is IOException
}
.collect { /* … */ }Combining with Exponential Backoff
Often, you don't just want to retry immediately — you want to wait, maybe increasing the wait time with each failed attempt. That helps reduce load, avoid "hammering" a failing service, etc.
Here's how you might implement exponential backoff:
flowOfData()
.retry(retries = 4) { cause ->
if (cause is IOException) {
var currentDelay = 1000L
val factor = 2L
delay(currentDelay)
// Increase delay for next time
currentDelay *= factor
true
} else {
false
}
}
.catch { error -> /* final handling */ }
.collect { value -> /* success path */ }You can adapt this:
- start with small delay (e.g. 500ms)
- increase multiplicatively (e.g. factor of 2 or 1.5)
- optionally cap the delay to a maximum
When to Use Which

Best Practices
- Don't infinite retry blindly — always think about max retries or some exit condition.
- Prefer delays/backoff — avoid retrying too quickly.
- Handle final failure with
.catch { … }so issues don't crash your app silently. - Be specific about exception types — retrying on any exception may mask serious bugs.
- Consider resource implications — long-running flows with many retries can tie up threads, memory.
Example Scenario
Imagine you have a function that fetches user data from a remote API using Retrofit, returning a Flow<User>. Sometimes the network fails. You want to retry up to 3 times with increasing delays, but only when the failure is due to IOException.
fun fetchUserFlow(): Flow<User> = flow {
val user = api.getUser()
emit(user)
}
fun fetchUserWithRetries(): Flow<User> =
fetchUserFlow()
.retry(retries = 3) { cause ->
if (cause is IOException) {
delay(500) // first retry after 500ms
true
} else {
false
}
}
.catch { e ->
// Show error to UI or log
throw e
}If you want to increase delay each retry:
fun fetchUserWithBackoff(): Flow<User> =
fetchUserFlow()
.retryWhen { cause, attempt ->
if (cause is IOException && attempt < 3) {
val delayTime = 500L * (2.0.pow(attempt.toDouble())).toLong()
delay(delayTime)
true
} else {
false
}
}
.catch { e -> /* … */ }Conclusion
Retry operators are essential tools in your Kotlin Flow toolkit. They help make your reactive or flow-based code resilient to intermittent failures. Use:
retryfor simpler retry needsretryWhenwhen you need more control (check attempt number, use delays, etc.)
Pair them with good delay strategies (exponential backoff), clear exception handling, and limits to avoid runaway retries.