Handling exception thrown within a withContext() in Android coroutine - android

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.

Related

Emit Exception in Kotlin Flow Android

I have emit exception inside flow and got below exception.
IllegalStateException: Flow exception transparency is violated:
Previous 'emit' call has thrown exception java.lang.NullPointerException, but then emission attempt of value 'planetbeyond.domain.api.Resource$Error#85b4d28' has been detected.
Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
For a more detailed explanation, please refer to Flow documentation.
at kotlinx.coroutines.flow.internal.SafeCollector.exceptionTransparencyViolated(SafeCollector.kt:140)
at kotlinx.coroutines.flow.internal.SafeCollector.checkContext(SafeCollector.kt:104)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:83)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
at planetbeyond.domain.use_cases.OptionSelectedCountUsecase$invoke$1.invokeSuspend(OptionSelectedCountUsecase.kt:20)
OptionSelectedCountUsecase.kt
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
emit(Resource.Success(data))
} catch (e: Exception) {
emit(Resource.Error(e.toString()))// crashed at this line when api don't response anything or some sort of server error
}
}
}
Repository.kt
interface Repository{
suspend fun getOptionSelectedCount(questionId: Int):List<OptionSelectedCountModel>
}
RepositoryImpl.kt
class RepositoryImpl #Inject constructor(
private val apiService: ApiService
) : Repository {
override suspend fun getOptionSelectedCount(questionId: Int): List<OptionSelectedCountModel> {
return apiService.getOptionSelectedCount(questionId).data.map {
it.toModel()
}
}
}
ApiService.kt
interface ApiService {
#GET("get_option_selected_count")
suspend fun getOptionSelectedCount(
#Query("question_id") question_id: Int
): WebResponse<List<OptionSelectedCountDto>>
}
LiveShowQuestionViewModel.kt
#HiltViewModel
class LiveShowQuestionsViewModel #Inject constructor(
private val optionSelectedCountUsecase: OptionSelectedCountUsecase
) : ViewModel() {
fun getOptionSelectedCount(questionId: Int) {
optionSelectedCountUsecase(questionId).onEach {
when (it) {
is Resource.Loading -> {
_optionSelectedCountState.value = OptionSelectedCountState(isLoading = true)
}
is Resource.Error -> {
_optionSelectedCountState.value = OptionSelectedCountState(error = it.message)
}
is Resource.Success -> {
_optionSelectedCountState.value = OptionSelectedCountState(data = it.data)
}
}
}///.catch { } // Why must I have to handle it here
.launchIn(viewModelScope)
}
}
Is it neccessary to handle exception outside flow like commented above. What is the best practice.
The problem is that you wrapped an emit call in try and try to emit in the matching catch block. This means that if the emit call itself throws (which ambiguously could be caused by some downstream problem with the flow) it's being instructing to emit again. This is very ambiguous and fragile behavior.
Instead, you can move your emit call(s) outside the try/catch:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
val result = try {
val data = repository.getOptionSelectedCount(questionId)
Resource.Success(data)
} catch (e: Exception) {
Resource.Error(e.toString())
}
emit(result)
}
}
Somehow, you're causing a NullPointerException in your collector. That's a separate problem to solve.
The root problem is that your
emit(Resource.Success(data))
throws an exception. When you catch that exception you are still in the "emit" block and you are trying to
emit(Resource.Error(e.toString())
So it's like emit inside emit. So yes this is wrong.
But let's get a step backward. Why there is an exception during the first emit? It looks like this data object is not properly filled with data, probably because of the issues that you mentioned (bad response etc), after it reaches the collector there is null pointer exception.
So basic flow should be
try to make the call, and catch http/parsing exception if there is one ( emit failure)
If there was no exception, validate if the object contains proper fields. If data is inconsistent emit Error
If everything is ok emit success
for example:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
emit(Resource.Success(data))
}else{
// some data integrity issues, missing fields
emit(Resource.Error("TODO error")
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
emit(Resource.Error(e.toString()))
}
}
}
This ideally should be split into, to not mess with exception catching of emit:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
emit(getResult(questionId))
}
fun getResult(questionId: Int): Resource<List<OptionSelectedCountModel>>{
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
return Resource.Success(data)
}else{
// some data integrity issues, missing fields
return Resource.Error("TODO error"
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
return Resource.Error(e.toString())
}
}
}
You should not emit exceptions and errors manually. Otherwise the user of the flow will not know, if exception actually happened, without checking the emitted value for being an error.
You want to provide exception transparency, therefore it is better to process them on collecting the flow.
One of the ways is to use catch operator. To simplify flow collecting we will wrap the catching behavior in a function.
fun <T> Flow<T>.handleErrors(): Flow<T> =
catch { e -> showErrorMessage(e) }
Then, while collecting the flow:
optionSelectedCountUsecase(questionId)
.onEach { ... }
.handleErrors()
.launchIn(viewModelScope)
Note, that if you want to process only the errors from invocation of the use case, you can change the order of operators. The previous order allows you to process errors from onEach block too. Example below will only process errors from use case invocation.
optionSelectedCountUsecase(questionId)
.handleErrors()
.onEach { ... }
.launchIn(viewModelScope)
Read more about exception handling in flows

Why does kotlin flow not trigger the transform function after an error is handled

I have the below code in my view model class.
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableStateFlow(0)
val marketState: LiveData<State<Market>> =
retry.flatMapLatest{repo.refreshMarket()}
.map { State.Success(it) as State<T> }
.catch { error -> emit(State.Error(error)) }
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.value++
}
}
MarketRepository.kt:
fun refreshMarket() =
flow { emit(api.getMarkets()) }
.onEach { db.upsert(it) }
.flowOn(dispatchers.IO)
It works fine until a network error occurs in the repository method refreshMarket then when I call the retry() on the view model, it doesn't trigger the flatMapLatest transformer function anymore on the retry MutableStateFlow, why?
Does the flow get complete when it calls a Catch block? how to handle such situation?
You're right, catch won't continue emitting after an exception is caught. As the documentation says, it is conceptually similar to wrapping all the code above it in try. If there is a loop in a traditional try block, it does not continue iterating once something is thrown, for example:
try {
for (i in 1..10) {
if (i == 2) throw RuntimeException()
println(i)
}
} catch (e: RuntimeException) {
println("Error!")
}
In this example, once 2 is encountered, the exception is caught, but code flow does not return to the loop in the try block. You will not see any numbers printed that come after 2.
You can use retryWhen instead of catch to be able to restart the flow. To do it on demand like you want, maybe this strategy could be used (I didn't test it):
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableSharedFlow<Unit>()
val marketState: LiveData<State<Market>> =
repo.refreshMarket()
.map { State.Success(it) as State<T> }
.retryWhen { error, _ ->
emit(State.Error(error))
retry.first() // await next value from retry flow
true
}
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.tryEmit(Unit)
}
}

Cannot emit data using flow or channelFlow on Android

I'm trying to implement One Tap, so I have created a function that looks like this:
override suspend fun oneTapSgnInWithGoogle() = flow {
try {
emit(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Result.Success(result))
} catch (e: Exception) {
Log.d(TAG, "oneTapSgnInWithGoogle: ${e.message}")
emit(Result.Error(e.message!!))
}
}
If I use flow and try to emit the result, my app crashed with the following message:
Flow exception transparency is violated:
StandaloneCoroutine has completed normally; but then emission attempt of value 'Error(message=StandaloneCoroutine has completed normally)' has been detected.
However, if change the code to:
override suspend fun oneTapSgnInWithGoogle() = channelFlow {
try {
send(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
send(Result.Success(result))
} catch (e: Exception) {
Log.d(TAG, "oneTapSgnInWithGoogle: ${e.message}")
send(Result.Error(e.message!!))
}
}
And I use channelFlow and try to send the result, the app isn't crashing but I still get the error message saying:
StandaloneCoroutine has completed normally
How can I emit the result correctly and get rid of this error message?
P.S. In my ViewModel class I use:
fun oneTapSgnInWithGoogle() = liveData(Dispatchers.IO) {
viewModelScope.launch {
repo.oneTapSgnInWithGoogle().collect { result ->
emit(result)
}
}
}
This is not a good practice to launch a coroutine in liveData block. liveData block is a suspend lambda, you can collect values directly in it without launching a coroutine:
fun oneTapSgnInWithGoogle() = liveData(Dispatchers.IO) {
repo.oneTapSgnInWithGoogle().collect { result ->
emit(result)
}
}
In your case liveData block has already finished execution (and corresponding coroutine, in which liveData block is executed) when you try to emit a value to LiveData. The solution above should solve the problem.

Kotlin coroutines crash with no helpful stacktrace

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.
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 {
}

Kotlin coroutines - gracefully handling errors from suspend functions

Trying to implement graceful handling of the errors with suspend functions that are called from async methods, How to catch the error thrown by a suspend method.
suspend fun findById(id: Long): User? {
throw Exception("my exception") // intentionally throwing to simulate error situation.
return userModel.findById(id) // IO, may throw an error
}
Caller piece, launching with IO thread
GlobalScope.launch(Dispatchers.IO) {
try {
var userAsync: Deferred<User?>? = null
arguments?.getLong("id")?.let {
userAsync = async { viewModel?.findById(it) } // async for efficiency as i've other async methods too.
}
val data = userAsync?.await()
withContext(Dispatchers.Main) {
user = data // data binding, populating UI fields of user
}
} catch (exception: Exception) {
withContext(Dispatchers.Main) { fault(exception) }
}
}
fault method
private fun fault(exception: Exception) {
Log.d("User", "fault: ${exception.localizedMessage}") // expecting output
}
Currently runtime is crashing, want to implement graceful handling of errors.
Attempt 2
Tried placing try catch within the async block but it didn't like it.
var userAsync: Deferred<UserVO?>? = null
arguments?.getLong("id")?.let {
userAsync = async {
try {
delegate?.findById(it)
} catch (e: Exception) {
print(e)
}
}
}
I would use a CoroutineExceptionHandler to make your coroutines fail gracefully:
1) Define the handler:
val exceptionHandler = CoroutineExceptionHandler { context, error ->
// Do what you want with the error
Log.d(TAG, error)
}
2) Refactor your findById function to be executed within an IO context and make your ui code main safe:
suspend fun findById(id : Int) : User? = withContext(Dispatchers.IO){
when(id){
0 -> throw Exception("not valid")
else -> return#withContext User(id)
}
}
Launch your job within MainScope (and so update the ui), passing exceptionHandler to launch coroutine builder in order to catch the exception:
val exceptionHandler = CoroutineExceptionHandler { _, error ->
// Do what you want with the error
Log.d(TAG, error)
}
MainScope().launch(exceptionHandler) {
val user = delegate?.findById(userId)
user?.let {
Timber.v(it.toString())
}
}

Categories

Resources