Kotlin coroutines crash with no helpful stacktrace

Viewed 1186

My Android app crashes and I see this stack trace in Logcat. It doesn't tell me which line of code is causing the problem.

2021-05-05 09:13:33.143 1069-1069/com.mycompany.app E/AndroidRuntime: FATAL EXCEPTION: main
  Process: com.mycompany.app, PID: 1069
  retrofit2.HttpException: HTTP 403 
    at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
    at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
    at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:919)

Is there a way to map this back to my code, to see which call to retrofit is causing it? I have a repository with code like this:

suspend fun getSomeData(): Stuff {
   return withContext(Dispatchers.IO) {
      val body = myRetroApi.getStuff()
      ...

Do I need to wrap every withContext body to make sure no Throwables escape? I thought that if something threw an exception there, it would log an error, not crash the entire app.

Edit

I messed up when asking this question and put the emphasis on wrong things. So I'm removing the "retrofit" tag. It turns out the withContext(Dispatchers.IO) call does re-throw the Exception as expected, but when the exception gets back up to viewModelScope.launch, if that block does not catch it, the app crashes.

2 Answers

If the exception is not handled the app will crash of course.

You can add a try catch to avoid this:

suspend fun getSomeData() {
   withContext(Dispatchers.IO) {
      try{
        val body = myRetroApi.getStuff()
        ...
      } catch (e : Exception){
         //your code
      }
...

Retrofit is giving you a 403 Unauthorized HTTP exception. It may be that the server isn't passing any additional error message or that you need to catch HttpException and check for the message. In either case, this isn't a Retrofit issue hence it's just passing the error it's getting from the server you're calling.

It's best to create a network result wrapper and a wrapper function for API calls to handle exceptions.

You can do something like this. Keep in mind, the actual implementation is completely up to you. I would however suggest using runCatching when it comes to couroutines as it handles cancellation exceptions.

 sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error(val exception: Throwable, val message: String?) : NetworkResult<Nothing>()
 }
    suspend fun networkCall(): String = ""
suspend fun <T> safeApiCall(block: suspend () -> T): NetworkResult<T> {

    return runCatching {
        withContext(Dispatchers.IO) {
            block()
        }
    }.fold({
        NetworkResult.Success(it)
    }, {
        when (it) {
            is HttpException -> NetworkResult.Error(it, "Network error")
            else -> NetworkResult.Error(it, "Some other message...")
            // else -> throw it
        }
    })
}

suspend fun getData() {
    val result: NetworkResult<String> = safeApiCall {
        networkCall()
    }
    when (result) {
        is NetworkResult.Success -> {
            //Handle success
        }
        is NetworkResult.Error -> { //Handle error
        }
    }
}


runCatching uses Kotlin's built-in Result class and there are several ways of handling the result. These are just a few.

    runCatching {
        //.....
    }.getOrElse { throwable ->
        //handle exception
    }

    runCatching {
        //.....
    }.getOrThrow()

    runCatching {

    }.onSuccess {

    }.onFailure {

    }
Related