Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using try catch block in swallowing exceptions when using kotlin coroutines

kotlin coroutines version 1.3.8
kotlin 1.3.72

This is the first time I am using coroutines and I have converted my rxjava2 with using coroutines. But as this is my first time I am wondering if I am following best practices.

  1. One question I have is catching the exceptions, as in kotlin this could be bad practice as swallowing exceptions might hide an servious bug. But using coroutines is there any other way to capture errors. In RxJava this is simple using the onError.

  2. Would this make it easier for testing?

  3. Is this the correct use of suspend functions?

Many thanks for any suggestions.

interface PokemonService {
    @GET(EndPoints.POKEMON)
    suspend fun getPokemons(): PokemonListModel
}

Interactor that will timeout after 10 seconds if the response is too slow or some network error

class PokemonListInteractorImp(private val pokemonService: PokemonService) : PokemonListInteractor {
    override suspend fun getListOfPokemons(): PokemonListModel {
        return withTimeout(10_000) {
            pokemonService.getPokemons()
        }
    }
}

Inside my view model I use the viewModelScope. Just wondering if I should be catching exceptions.

fun fetchPokemons() {
    viewModelScope.launch {
        try {
            shouldShowLoading.value = true
            pokemonListLiveData.value = pokemonListInteractor.getListOfPokemons()
        }
        catch(error: Exception) {
            errorMessage.value = error.localizedMessage
        }
        finally {
            shouldShowLoading.value = false
        }
    }
}

In my fragment I am just observing the live data and populating the adapter.

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
   bindings = FragmentPokemonListBinding.inflate(inflater, container, false)

    setupAdapter()
    pokemonViewModel.registerPokemonList().observe(viewLifecycleOwner, Observer { pokemonList ->
        pokemonAdapter.populatePokemons(pokemonList.pokemonList)
    })

    return bindings.root
}
like image 891
ant2009 Avatar asked Jul 25 '20 19:07

ant2009


2 Answers

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
}
like image 51
Sergey Avatar answered Oct 03 '22 14:10

Sergey


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

like image 40
Laksitha Ranasingha Avatar answered Oct 03 '22 15:10

Laksitha Ranasingha