Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exception not being caught in Coroutines

I can't seem to get my error-handling done in coroutines. I've been reading lots of articles and the exception handling documentation but I can't seem to get it working.

Here's my setup:

My ViewModel launches the coroutine with it's scope

class MyViewModel(private var myUseCase: MyUseCase) : ViewModel() {
    private val viewModelJob = Job()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

    fun doSomething() {
        uiScope.launch {
            try {
                myUseCase()
            } catch (exception: Exception) {
                // Do error handling here
            }
        }
    }
}

My UseCase just handles a few logic and in this case a validator of some sort

class MyUseCase(private val myRepository: MyRepository) {
    suspend operator fun invoke() {
        if (checker()) {
            throw CustomException("Checker Failed due to: ...")
        }

        myRepository.doSomething()
    }
}

Then my Repository just handles the network layer / local layer

object MyRepository {
    private val api = ... // Retrofit

    suspend fun doSomething() = api.doSomething()
}

And here's my Retrofit interface

interface MyInterface {
    @POST
    suspend fun doSomething()
}

The try/catch from the ViewModel can handle the error from the Retrofit call however, it can't catch the error from the CustomException thrown by the UseCase. From articles I've been reading, this should work. If I use async I can do await and consume the error but I don't have to use async in this case and I've been wrapping my head around this. I might be getting lost.

Any help would be greatly appreciated! Thanks in advance!

Edit:

Here's the error log I'm getting:

com.example.myapp.domain.errors.CustomException
        at com.example.myapp.domain.FeatureOne$invoke$2.invokeSuspend(FeatureOne.kt:34)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)

The error directly points to the explicit throw statement.

like image 947
Kurt Acosta Avatar asked May 22 '19 06:05

Kurt Acosta


People also ask

How do you handle exceptions in coroutines?

Coroutines use the regular Kotlin syntax for handling exceptions: try/catch or built-in helper functions like runCatching (which uses try/catch internally). We said before that uncaught exceptions will always be thrown. However, different coroutines builders treat exceptions in different ways.

How to define a coroutineexceptionhandler?

Here’s how you can define a CoroutineExceptionHandler, whenever an exception is caught, you have information about the CoroutineContext where the exception happened and the exception itself: When ⏰: The exception is thrown by a coroutine that automatically throws exceptions (works with launch, not with async ).

Why can’t a try-catch clause be used in a coroutine?

If a Coroutine doesn’t handle exceptions by itself with a try-catch clause, the exception isn’t re-thrown and can’t, therefore, be handled by an outer try-catch clause. Instead, the exception is “propagated up the job hierarchy” and can be handled by an installed CoroutineExceptionHandler.

What is the difference between async and coroutine exception handling?

In Coroutines started with async, uncaught exceptions are also immediately propagated up the job hierarchy. But in contrast to Coroutines started with launch, the exceptions aren’t handled by an installed CoroutineExceptionHandler and also aren’t passed to the thread’s uncaught exception handler.


2 Answers

Trying with CoroutineExceptionHandler can be workaround for handling exceptions inside coroutines.

CoroutineExceptionHandler context element is used as generic catch block of coroutine where custom logging or exception handling may take place. It is similar to using Thread.uncaughtExceptionHandler.

How to use it?

val handler = CoroutineExceptionHandler { _, exception -> 
    println("Caught $exception") 
}
val job = GlobalScope.launch(handler) {
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
    throw ArithmeticException() // Nothing will be printed, relying on user to call 
    deferred.await()
}
joinAll(job, deferred)

In your ViewModel, make sure that your uiScope is using SupervisorJob rather than Job. SupervisorJob's can handle its children's failure individually. Job would get cancelled unlike SupervisorJob

If you're using 2.1.0 for AAC Lifecycle and ViewModel, use the viewModelScope extension instead.

like image 104
Jeel Vankhede Avatar answered Sep 21 '22 15:09

Jeel Vankhede


Another way to resolve this would be to covert your custom error object to implement CancellationException

For eg:

Your CustomException can be implemented as :

sealed class CustomError : CancellationException() {
        data class CustomException(override val message: String = "Checker Failed due to: ...") : CustomError
}

This exception would get caught in the try/catch block of the view model

like image 27
Burhan Shakir Avatar answered Sep 19 '22 15:09

Burhan Shakir