Using try catch block in swallowing exceptions when using kotlin coroutines

Viewed 5336
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
}
5 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
}

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

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.

To answer your questions:

  1. Yes you must catch network exceptions if you don't want your app to crash when the user turns off the Wifi! Rxjava's onError lambda is equivalent to a try/catch block in kotlin coroutines (although I prefer the runCatching {}.onFailure {} syntactic sugar).
  2. Do you mean is coroutines easier to test that RxJava? I'd say they are similar, but there isn't as much info available on the internet about testing coroutines yet.
  3. The only problem I see with your usage of the suspend function is that you're running it on the main thread, see below:

Here is how I would write your fetchPokemons function:

fun fetchPokemons() {
    viewModelScope.launch {
        shouldShowLoading.value = true
        runCatching { 
            // Inject ioDispatcher into this class, so you can replace it with testDispatcher in tests
            withContext(ioDispatcher) {
                pokemonListInteractor.getListOfPokemons() // This happens on IO dispatcher
            }
        }.onSuccess { pokemonList ->
            pokemonListLiveData.value = pokemonList // This happens on Main (UI) dispatcher
        }.onFailure {
            errorMessage.value = error.localizedMessage // On Main dispatcher too
        }
        
        // Finally block not needed since this will wait for the suspending function above
        shouldShowLoading.value = false
    }
}

This is the basic approach, however there are good reasons to go one step further and wrap your PokemonListModel in a Result type. You could:

The main advantage is it forces everyone consuming your PokemonListInteractor to think about handling the error case. Kotlin not having checked exceptions makes a Result type more necessary, as it is easy to loose track of where errors need to be handled with the above approach.

I find this code to resolve my problem

see reference

Answer #6: I'm posting this for two reasons:

I personnaly tried increasing connection timeout but, evetually, it doesn't really solve the problem at its root. Besides, user is not supposed to wait for longer than 10 seconds according to this post. In a real world application, we would rather do our best to implement a solution in as clean as possible way. So, here's a solution that I came up with in Kotlin. It's inspired from the answer provided by @Olcay Ertaş and combined with Google's recommended architecture for Android apps.

//Create a TimeoutInterceptor interface:
interface TimeoutInterceptor : Interceptor
//Implement the TimeoutInterceptor interface:

 class TimeoutInterceptorImpl : TimeoutInterceptor {

     override fun intercept(chain: Interceptor.Chain): Response {
         if (isConnectionTimedOut(chain))
             throw SocketTimeoutException()
         return chain.proceed(chain.request())
     }

     private fun isConnectionTimedOut(chain: Interceptor.Chain): Boolean {
         try {
             val response = chain.proceed(chain.request())
             val content = response.toString()
             response.close()
             Log.d(tag, "isConnectionTimedOut() => $content")
         } catch (e: SocketTimeoutException) {
             return true
         }
         return false
     }
 }

In your ApiService interface, add the TimeoutInterceptor to the OkHttpClient builder:

 val okHttpClient = OkHttpClient.Builder()
         .addInterceptor(requestInterceptor)
         // Add timeout interceptor
         .addInterceptor(timeoutInterceptor)
         // Set a 5s custom connect timout
         .connectTimeout(5, TimeUnit.SECONDS)
         .build()

As you might have noticed, you can set a custom connect timeout. Otherwise, it's left to 10 seconds as a default value according to the documentation.

Create an enum class ConnectionState. It will provide an enum constant object CONNECTION_TIMEOUT which will be used further to convey the appropriate connection (or API call) state from EntityNetworkDataSource

class to the View class (if you follow Google's MVVM pattern):

 enum class ConnectionState {
     CONNECTED, NOT_CONNECTED, CONNECTION_TIMEOUT
 }

Assuming your EntityNetworkDataSource interface would look something like this:

 interface EntityNetworkDataSource {
     val fetchedEntity: LiveData<Entity>

     // Wrap your ConnectionState object in LiveData in order to be able to observe it in the View
     val connectionState: LiveData<ConnectionState>

     // Fetch `Entity` object from the network
     suspend fun fetchEntity(id: Int)
 }

In the EntityNetworkDataSource implementation class, you can properly catch the SocketTimeoutException as shown below, inside the fetchEntity(id: Int) implementation:

 class EntityNetworkDataSourceImpl(
         private val apiService: ApiService
 ) : EntityNetworkDataSource {

     private val _fetchedEntity = MutableLiveData<Entity>()

     override val fetchedEntity: LiveData<Entity>
         get() = _fetchedEntity

     // We want to keep our MutableLiveData private because they can be changed.
     // So we want to be able to update them only from the inside of this class
     private val _connectionState = MutableLiveData<ConnectionState>()

     override val connectionState: LiveData<ConnectionState>
         get() = _connectionState

     override suspend fun fetchEntity(id: Int) {
         try {
             val fetchedEntity = apiService
                     .getEntity(id)
                     .await()

             // Convey the updated connection state to the observer View
             _connectionState.postValue(ConnectionState.CONNECTED)

             _fetchedEntity.postValue(fetchedEntity)
         } catch (e: SocketTimeoutException) {
             Log.e(tag, "Connection timeout. ", e)
             // Catch the SocketTimeoutException and post the updated connection state to the observer View
             _connectionState.postValue(ConnectionState.CONNECTION_TIMEOUT)
         }
     }
 }

The same principle applies to any connection exception you wana intercept and catch.

Answered By: AndroWeed

See Image

Related