kaneda
kaneda

Reputation: 6187

Why `async` doesn't inherit SupervisorJob if called directly inside the coroutine block?

Given the code snippets below:

Snippet [1]

val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.IO + job)
scope.launch {
   try {
     scope.async {
       throw RuntimeException("Oops!")
     }.await()
   } catch(e: Exception) {
    // Handle exception
   }
}

and Snippet [2]

val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.IO + job)
scope.launch {
   try {
     async {
       throw RuntimeException("Oops!")
     }.await()
   } catch(e: Exception) {
    // Handle exception
   }
}

The first one works, and second snippet crashes. The general explanation is that in the first case, async inherit the SupervisorJob from scope, and in the second case no.

My question is, if async is an extension function of CoroutineScope, why in the second case (which crashes) it doesn't inherit the SupervisorJob in the same way?

Upvotes: 4

Views: 918

Answers (3)

Dennis Nguyen
Dennis Nguyen

Reputation: 474

As I see, newer versions of coroutines both of your snippets will work. Just wrap the function await() with try/catch block.

Upvotes: 0

Marko Topolnik
Marko Topolnik

Reputation: 200206

In your first snippet, because you explicitly override the scope established by the launch builder, there is no parent-child relationship between it and the async block you have inside it.

This is an equivalent way to write the first snippet:

val deferred = scope.async {
    throw RuntimeException()
}

scope.launch {
    try {
        deferred.await()
    } catch(e: Exception) {
    }
}

The async coroutine fails with an exception. Its parent is a SupervisorJob, which ignores the failure. The launch coroutine calls Deferred.await(), which throws an exception. You catch the exception and nothing fails.

Your second snippet can be rewritten as follows:

scope.launch {
    val innerScope = this

    innerScope.async {
        throw RuntimeException()
    }
}

Here you can see explicitly which scope the async block inherits. It is not the top-level one with SupervisorJob, but the inner one that launch created. When the async coroutine fails, the parent job reacts to it by first cancelling all the other children (none in this case) and then itself. The deferred.await() statement makes no difference here, which is why I removed it. The launch coroutine will automatically wait for all the child coroutines to complete anyway.

Upvotes: 2

Matt Timmermans
Matt Timmermans

Reputation: 59253

launch creates a new Job, and propagates that into the CoroutineScope that the block receives. In the second snippet, the async job is a child of the launch Job, and the launch job is cancelled when its child fails.

In the first snippet, the async job is a child of the SupervisorJob you made, which is not cancelled when its child fails. The launch job in this case has no children. It just catches the exception and completes.

Upvotes: 2

Related Questions