Using try catch block in swallowing exceptions when using kotlin coroutines
Solution 1:
I suggest to use sealed
Result
class and try/catch
block to handle api response exceptions:
sealed class Result<out T : Any>
class Success<out T : Any>(val data: T) : Result<T>()
class Error(val exception: Throwable, val message: String = exception.localizedMessage) : Result<Nothing>()
inline fun <T : Any> Result<T>.onSuccess(action: (T) -> Unit): Result<T> {
if (this is Success) action(data)
return this
}
inline fun <T : Any> Result<T>.onError(action: (Error) -> Unit): Result<T> {
if (this is Error) action(this)
return this
}
Catch exceptions in PokemonListInteractorImp
using try/catch
block and return appropriate Result
:
class PokemonListInteractorImp(private val pokemonService: PokemonService) : PokemonListInteractor {
override suspend fun getListOfPokemons(): Result<PokemonListModel> {
return withTimeout(10_000) {
try {
Success(pokemonService.getPokemons())
} catch (e: Exception) {
Error(e)
}
}
}
}
In your ViewModel
you can use extension functions onSuccess
, onError
on Result
object to handle the result:
fun fetchPokemons() = viewModelScope.launch {
shouldShowLoading.value = true
pokemonListInteractor.getListOfPokemons()
.onSuccess { pokemonListLiveData.value = it }
.onError { errorMessage.value = it.message }
shouldShowLoading.value = false
}
Solution 2:
As you use launch
coroutine builder, it bubbles up the exceptions. So I think CoroutineExceptionHandler
will be an alternative way of handling uncaught exceptions in a more idiomatic way. The advantages are
- the exceptions thrown inside coroutines won't be swallowed and you have better visibility
- you can cleanly test exception propagation and handling (if you implement the exception handler) in coroutines
- you can reduce/avoid boilerplate try/catch
Take a look at this example; I have tried to showcase a few scenarios;
/**
* I have injected coroutineScope and the coroutineExceptionHandler in the constructor to make this class
* testable. You can easily mock/stub these in tests.
*/
class ExampleWithExceptionHandler(
private val coroutineScope: CoroutineScope = CoroutineScope(
Executors.newFixedThreadPool(2).asCoroutineDispatcher()
),
private val coroutineExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
println(
"Exception Handler caught $throwable, ${throwable.suppressed}" //you can get the suppressed exception, if there's any.
)
}
) {
/**
* launch a coroutine with an exception handler to capture any exception thrown inside the scope.
*/
fun doWork(fail: Boolean): Job = coroutineScope.launch(coroutineExceptionHandler) {
if (fail) throw RuntimeException("an error...!")
}
}
object Runner {
@JvmStatic
fun main(args: Array<String>) {
val exampleWithExceptionHandler = ExampleWithExceptionHandler()
//a valid division, all good. coroutine completes successfully.
runBlocking {
println("I am before doWork(fail=false)")
exampleWithExceptionHandler.doWork(false).join()
println("I am after doWork(fail=false)")
}
//an invalid division. Boom, exception handler will catch it.
runBlocking {
println("I am before doWork(fail=true)")
exampleWithExceptionHandler.doWork(true).join()
println("I am after doWork(fail=true)")
}
println("I am on main")
}
}
Output
I am before doWork(fail=false)
I am after doWork(fail=false)
I am before doWork(fail=true)
Exception Handler caught java.lang.RuntimeException: an error...!, [Ljava.lang.Throwable;@53cfcb7a
I am after doWork(fail=true)
I am on main
You can see the exception has been captured by the handler successfully. If coroutines are nested, you can get the inner exception with suppressed
method.
This approach is good for non-async coroutines. The async
coroutines are a different beast. If you try to await
on an async
coroutine inside the same runBlocking
code, the exceptions won't be handled propagated like launch
type. It will still throw out of the scope and kills the main thread. For async, I saw that you can use supervisorScope
or wrapped coroutine (which I haven't got a chance to use).
As propagated unhandled exceptions can be handled globally. This style can help you with reuse of exception handler code and any subsequent operations. For example, the docs suggest;
Normally, the handler is used to log the exception, show some kind of error message, terminate, and/or restart the application.
A similar approach can be found when you use Spring framework with global exception handlers.
Possible drawbacks are;
- This is only suitable for uncaught exceptions and is not recoverable
- This may look like AOP style code
- Returning different values based on exceptions could concentrate the logic in the exception handler.
- Have to have good understand of how exceptions are propagated depending on the coroutine builders and scopes use
About suspension, If your API/functions are fully async, you can return the Job
or Deferred<T>
created by the coroutine scope. Otherwise, you have to block somewhere in your code to complete the coroutine and return the value.
This doc is very useful https://kotlinlang.org/docs/reference/coroutines/exception-handling.html
Another good resource specific to Android apps - https://alexsaveau.dev/blog/kotlin/android/2018/10/30/advanced-kotlin-coroutines-tips-and-tricks/#article
Solution 3:
In your PokemonListInteractorImp class, handle response exception and do with it whatever you wanna. In ViewModel, where you set value to some LiveData object your List, this should already be success state. Try something like:
protected suspend fun <T> requestApiCall(call: suspend () -> T): Either<FailureState, T> {
return try {
Either.Right(call.invoke())
} catch (e: HttpException) {
return Either.Left(FailureState.ServerError)
} catch (e: UnknownHostException) {
return Either.Left(FailureState.NetworkConnection)
} catch (e: Throwable) {
e.printStackTrace()
return Either.Left(FailureState.UnknownError)
}
}
Failure state class:
sealed class FailureState {
object NetworkConnection : FailureState()
object ServerError : FailureState()
object UnknownError : FailureState()
/** * Extend this class for feature specific failures.*/
abstract class FeatureFailure: FailureState()
}
ViewModel, something like:
fun loadQuestions(type: String) {
viewModelScope.launch {
questionsUseCase.invoke(type).fold(::handleError, ::handleUsersResponse)
}
}
private fun handleUsersResponse(questionsResponse: QuestionsResponse) {
questionsResponse.questions?.apply {
postScreenState(ShowQuestions(map { it.toDomainModel() }.toMutableList()))
}
}
Something like that, hope it helps. But, if you are looking just to handle exceptions in Coroutines, here is good source: https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c#:~:text=Coroutines%20use%20the%20regular%20Kotlin,treat%20exceptions%20in%20different%20ways.
If you have any question, just ask.