Implementing a Retry Mechanism with Coroutines in Kotlin for Network Failures

Mohamed Elsdody
Stackademic
Published in
4 min readMay 7, 2024

--

Introduction

In the digital age, seamless network communication is the backbone of many applications. Yet, network requests are prone to failures due to various unpredictable factors like server downtime or connectivity issues. This can lead to poor user experiences unless handled adeptly within the application’s codebase. Using Kotlin, a modern programming language favored for Android and server-side development, developers have powerful tools at their disposal to manage these scenarios effectively. One such tool is coroutines, which simplify asynchronous programming and error handling. This article will guide you through implementing a retry mechanism using Kotlin coroutines, ensuring your network requests are resilient and robust against failures.

Understanding Network Failures

Network failures can occur for a myriad of reasons — from simple timeout issues to complex server malfunctions. For developers, the key challenge is not just detecting these failures but responding to them in a way that maintains a smooth user experience. Implementing a retry mechanism is a fundamental strategy in this regard. It allows the application to attempt the same request multiple times before concluding it as a failure, thus mitigating temporary network issues and reducing the chance of an operation failing due to transient problems.

Basics of Kotlin Coroutines

Coroutines are a feature of Kotlin that allow for simplifying code that executes asynchronously. They provide an easier way to handle tasks that can take unknown amounts of time, like network requests, by suspending execution without blocking the thread they operate on. This makes coroutines a perfect fit for handling operations where you expect delays or the need for retries without freezing the user interface.

In the above example, runBlocking and launch are used to create a coroutine that includes a delay, illustrating how coroutines manage asynchronous tasks efficiently.

Implementing a Retry Mechanism with Coroutines

To effectively manage network requests, especially when they fail, a structured retry mechanism can be crucial. Below is a basic implementation using Kotlin coroutines:

import kotlinx.coroutines.delay

suspend fun <T> retryCoroutine(
maxAttempts: Int = 3,
initialDelay: Long = 1000, // Initial delay in milliseconds
maxDelay: Long = 3000, // Max delay in milliseconds
factor: Double = 2.0, // Factor to increase the delay each retry
block: suspend () -> T // The suspend function that makes the network request
): T {
var currentDelay = initialDelay
repeat(maxAttempts - 1) { attempt ->
try {
return block() // Try to execute the block
} catch (e: Exception) {
println("Attempt ${attempt + 1} failed: ${e.message}")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
}
return block() // Last attempt, throw exception if it fails
}

This function, retryCoroutine, encapsulates a robust retry logic using Kotlin coroutines. It accepts a block parameter, which is a suspend function that executes the network request. The function allows configuring the number of attempts, initial delay, maximum delay, and the factor by which the delay increases after each attempt. This exponential backoff approach is effective in managing retries by gradually increasing the wait time, thereby reducing the burden on the network and increasing the chances of recovery in case of temporary issues.

The Full Code Example :

class RetryNetworkRequestViewModel(
private val api: MockApi = mockApi()
) : BaseViewModel<UiState>()
{

fun performNetworkRequest() {
uiState.value = UiState.Loading
viewModelScope.launch {
val numberOfRetries = 2
try {
retry(times = numberOfRetries) {
val recentVersions = api.getRecentAndroidVersions()
uiState.value = UiState.Success(recentVersions)
}
} catch (e: Exception) {
uiState.value = UiState.Error("Network Request failed")
}
}
}

// retry with exponential backoff
// inspired by https://stackoverflow.com/questions/46872242/how-to-exponential-backoff-retry-on-kotlin-coroutines
private suspend fun <T> retry(
times: Int,
initialDelayMillis: Long = 100,
maxDelayMillis: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelayMillis
repeat(times) {
try {
return block()
} catch (exception: Exception) {
Timber.e(exception)
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelayMillis)
}
return block() // last attempt
}
}

Conclusion

Using Kotlin coroutines and the retryCoroutine function provides a structured and effective way to handle network request failures. This approach not only simplifies asynchronous error handling but also enhances application stability by intelligently managing retry attempts. It is an essential technique for developers looking to build fault-tolerant systems.

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

🚀 Mobile Application Developer | Android & iOS Specialist | Cross-Platform Innovator (React Native , Flutter , KMP)