Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling exception thrown within a withContext() in Android coroutine

I have an android app that I have built up an architecture similar to the Google IO App. I use the CoroutineUseCase from that app (but wrap results in a kotlin.Result<T> instead).

The main code looks like this:

suspend operator fun invoke(parameters: P): Result<R> {
        return try {
            withContext(Dispatchers.Default) {
                work(parameters).let {
                    Result.success(it)
                }
            }
        } catch (e: Throwable) {
            Timber.e(e, "CoroutineUseCase Exception on ${Thread.currentThread().name}")
            Result.failure<R>(e)
        }
    }

 @Throws(RuntimeException::class)
 protected abstract suspend fun work(parameters: P): R

Then in my view model I am invoking the use case like this:

 viewModelScope.launch {
            try {
                createAccountsUseCase(CreateAccountParams(newUser, Constants.DEFAULT_SERVICE_DIRECTORY))
                    .onSuccess {
                        // Update UI for success
                    }
                    .onFailure {
                        _errorMessage.value = Event(it.message ?: "Error")
                    }
            } catch (t: Throwable) {
                Timber.e("Caught exception (${t.javaClass.simpleName}) in ViewModel: ${t.message}")
            }

My problem is even though the withContext call in the use case is wrapped with a try/catch and returned as a Result, the exception is still thrown (hence why I have the catch in my view model code - which i don't want). I want to propagate the error as a Result.failure.

I have done a bit of reading. And my (obviously flawed) understanding is the withContext should create a new scope so any thrown exceptions inside that scope shouldn't cancel the parent scope (read here). And the parent scope doesn't appear to be cancelled as the exception caught in my view model is the same exception type thrown in work, not a CancellationException or is something unwrapping that?. Is that a correct understanding? If it isn't what would be the correct way to wrap the call to work so I can safely catch any exceptions and return them as a Result.failure to the view model.

Update: The implementation of the use case that is failing. In my testing it is the UserPasswordInvalidException exception that is throwing.

override suspend fun work(parameters: CreateAccountParams): Account {

        val tokenClient = with(parameters.serviceDirectory) {
            TokenClient(tokenAuthorityUrl, clientId, clientSecret, moshi)
        }

        val response = tokenClient.requestResourceOwnerPassword(
            parameters.newUser.emailAddress!!,
            parameters.newUser.password!!,
            "some scopes offline_access"
        )

        if (!response.isSuccess || response.token == null) {
            response.statusCode?.let {
                if (it == 400) {
                    throw UserPasswordInvalidException("Login failed. Username/password incorrect")
                }
            }
            response.exception?.let {
                throw it
            }
            throw ResourceOwnerPasswordException("requestResourceOwnerPassword() failed: (${response.message} (${response.statusCode})")
        }

        // logic to create account

        return acc
    }
}

class UserPasswordInvalidException(message: String) : Throwable(message)
class ResourceOwnerPasswordException(message: String) : Throwable(message)

data class CreateAccountParams(
    val newUser: User,
    val serviceDirectory: ServiceDirectory
)

Update #2: I have logging in the full version here is the relevant details:

2020-09-24 18:12:28.596 25842-25842/com.ipfx.identity E/CoroutineUseCase: CoroutineUseCase Exception on main
    com.ipfx.identity.domain.accounts.UserPasswordInvalidException: Login failed. Username/password incorrect
        at com.ipfx.identity.domain.accounts.CreateAccountsUseCase.work(CreateAccountsUseCase.kt:34)
        at com.ipfx.identity.domain.accounts.CreateAccountsUseCase.work(CreateAccountsUseCase.kt:14)
        at com.ipfx.identity.domain.CoroutineUseCase$invoke$2.invokeSuspend(CoroutineUseCase.kt:21)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2020-09-24 18:12:28.598 25842-25842/com.ipfx.identity E/LoginViewModel$createAccount: Caught exception (UserPasswordInvalidException) in ViewModel: Login failed. Username/password incorrect

The full exception is logged inside the catching in CoroutineUseCase.invoke. And then again the details logged inside the catch in the view model.

Update #3 @RKS was correct. His comment caused me to look deeper. My understanding was correct on the exception handling. The problem was in using the kotlin.Result<T> return type. I am not sure why yet but I was somehow in my usage of the result trigger the throw. I switched the to the Result type from the Google IO App source and it works now. I guess enabling its use as a return type wasn't the smartest.

like image 547
Nic Strong Avatar asked Aug 31 '25 05:08

Nic Strong


1 Answers

try/catch inside viewModelScope.launch {} is not required.

The following code is working fine,

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

class TestCoroutines {

    private suspend fun work(): String {
        delay(1000)
        throw Throwable("Exception From Work")
    }

    suspend fun invoke(): String {
        return try {
            withContext(Dispatchers.Default) {
                work().let { "Success" }
            }
        } catch (e: Throwable) {
            "Catch Inside:: invoke"
        }
    }

    fun action() {
        runBlocking {
            val result = invoke()
            println(result)
        }
    }
}

fun main() {
    TestCoroutines().action()
}

enter image description here

Please check the entire flow if same exception is being thrown from other places.

like image 106
Randheer Avatar answered Sep 02 '25 19:09

Randheer