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.
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()
}
Please check the entire flow if same exception is being thrown from other places.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With