Skip to content

Exception thrown by async is not caught when parent job is cancelled. #875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
yihuaqi opened this issue Dec 5, 2018 · 11 comments
Closed
Labels

Comments

@yihuaqi
Copy link

yihuaqi commented Dec 5, 2018

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?

@yihuaqi
Copy link
Author

yihuaqi commented Dec 5, 2018

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?

@Bluexin
Copy link

Bluexin commented Dec 5, 2018

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())

@qwwdfsad
Copy link
Collaborator

qwwdfsad commented Dec 5, 2018

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

@yihuaqi
Copy link
Author

yihuaqi commented Dec 6, 2018

@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.

@qwwdfsad
Copy link
Collaborator

qwwdfsad commented Dec 6, 2018

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())
}

@yihuaqi
Copy link
Author

yihuaqi commented Dec 7, 2018

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())

So is there any semantic difference between using GlobalScope.launch(contextJob) and delegating MainScope() to a CoroutineScope with contextJob?

@Bluexin
Copy link

Bluexin commented Dec 7, 2018

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)

@yihuaqi
Copy link
Author

yihuaqi commented Dec 9, 2018

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.

@awenger
Copy link

awenger commented Dec 11, 2018

@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.

@shakil807g
Copy link

@yihuaqi I have an exact same issue how do u solve that problem ?

@yihuaqi
Copy link
Author

yihuaqi commented Jan 4, 2019

@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.

@qwwdfsad qwwdfsad closed this as completed May 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants