Sample code for the scenario.
import kotlinx.coroutines.*
import java.io.IOException
import java.lang.Exception
val contextJob = Job()
fun main() {
GlobalScope.launch(contextJob) {
try {
apiRequest().await()
} catch(e: Exception) {
// Try to handle the exception gracefully
println("Exception caught")
}
}.apply {
// But parent job is canceled after a while
Thread.sleep(300)
contextJob.cancel()
}
Thread.sleep(3000L)
}
fun apiRequest() = GlobalScope.async(Dispatchers.IO) {
// Do some network request
Thread.sleep(1000)
// An IOException is thrown
throw IOException()
"API Result"
}
We have a contextJob in activity as the parent job of all coroutines which is cancelled in onDestroy(), as recommended in official guide.
The problem is that when the contextJob is cancelled while the async job is executing, the exception thrown by async will not be caught by the try...catch... block , and thus crashes our app.
I'm aware that CoroutineExceptionHandler can catch the exception, but then it brings a huge mental load during development. I would rather to not use contextJob at all.
When is the recommended way of dealing with this situation?
The stacktrace is like this
Exception in thread "DefaultDispatcher-worker-3" java.io.IOException
at ExceptionTestKt$tryException$1.invokeSuspend(ExceptionTest.kt:28)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:236)
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)
This issue starts to raise right after we upgrade kotlin to 1.3.0. I'm wondering if there's any behavior change that i'm not aware of?
From the docs on GlobalScope :
Application code usually should use application-defined [CoroutineScope], using
[async][CoroutineScope.async] or [launch][CoroutineScope.launch]
on the instance of [GlobalScope] is highly discouraged.
You shouldn't use GlobalScope, but rather make your own (a common way of doing it on android is to have your Activity implement CoroutineScope, delegating to MainScope() (MyActivity : MyAbstractActivity, CoroutineScope by MainScope())
When is the recommended way of dealing with this situation?
Using withContext instead of async in this particular use-case. You don't really need async here
@qwwdfsad Yeah, you're right in this particular case. But what if we do need async? There might be case where I want to do concurrent network requests.
Then recommended solution is to wrap them into coroutineScope {} or supervisorScope {}`.
We probably should update documentation in that place.
E.g.
suspend fun concurrentNetworkRequests() = coroutineScope {
val first = firstRequest()
val second = secondRequest()
combineResults(first.await(), second.await())
}
From the docs on GlobalScope :
Application code usually should use application-defined [CoroutineScope], using
[async][CoroutineScope.async] or [launch][CoroutineScope.launch]
on the instance of [GlobalScope] is highly discouraged.You shouldn't use
GlobalScope, but rather make your own (a common way of doing it on android is to have your Activity implementCoroutineScope, delegating toMainScope()(MyActivity : MyAbstractActivity, CoroutineScope by MainScope())
So is there any semantic difference between using GlobalScope.launch(contextJob) and delegating MainScope() to a CoroutineScope with contextJob?
When destroying your Activity, you should cancel it's scope, which means the children you launched will be cancelled as well (as to not leave orphaned background tasks running)
When destroying your Activity, you should cancel it's scope, which means the children you launched will be cancelled as well (as to not leave orphaned background tasks running)
Yeah I understand. contextJob is the same thing as MainScope in the old guide. The problem is once the parent scope is cancelled, all the async jobs will throw their exception in thread pool and cause crashes.
Actually I don't think binding a scope to activity can relieve developers of the complexity of asynchronous programming. In our practice we find that cancel parent job when destroying activity helps very little. We still need to check UI state after an async job, due to the fact that cancellation of coroutine is cooperative. And async job throwing exception on thread pool instead of being caught makes it even harder to use correctly.
@qwwdfsad I described a similar problem in #873, any I also tested coroutineScope, supervisorScope with the same effect, that the exception was not handled/caught.
@yihuaqi I have an exact same issue how do u solve that problem ?
@yihuaqi I have an exact same issue how do u solve that problem ?
We use add an CoroutineExceptionHandler for every coroutine we start to avoid crash for now.
Most helpful comment
Then recommended solution is to wrap them into
coroutineScope {}or supervisorScope {}`.We probably should update documentation in that place.
E.g.