Skip to content
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

CancellationException behaviour #2618

Closed
serddmit opened this issue Mar 30, 2021 · 7 comments
Closed

CancellationException behaviour #2618

serddmit opened this issue Mar 30, 2021 · 7 comments
Labels

Comments

@serddmit
Copy link

I checked docs for it and as I understand, this exception is not uncaught, it notifies the parent about cancelation and parent treat this exception as normal reason.

My question is - does it still propagates to next parent and possibly finishes in the handler, that ignores it?
Because it's not very clear from documentation side, how does it propagates.

Also, what this statement in the documentation mean:

Cancellation exceptions are transparent and are unwrapped by default:

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
    val inner = launch { // all this stack of coroutines will get cancelled 
        launch {
            launch {
                throw IOException() // the original exception
            } 
        }
    }
    try {
        inner.join()
    } catch (e: CancellationException) {
        println("Rethrowing CancellationException with original cause")
        throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
    } 
}
job.join()

Output:

 Rethrowing CancellationException with original cause 
 CoroutineExceptionHandler got java.io.IOException

What does it mean transparent and unwrapped?
This example show that cause will not be used in the handler:

import kotlinx.coroutines.*
import java.io.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            throw IOException() // the original exception
        }
        launch {
            throw CancellationException("Test", ArithmeticException("Arithmetic"))
        }
    }
    job.join()
}

Output:

CoroutineExceptionHandler got java.io.IOException

I found in the code base this functions:

private fun getFinalRootCause(state: Finishing, exceptions: List<Throwable>): Throwable?
private fun addSuppressedExceptions(rootCause: Throwable, exceptions: List<Throwable>)

But they ignore cancelation exceptions at all.

@elizarov
Copy link
Contributor

elizarov commented Apr 5, 2021

Transparent means that the original exception gets propagated up to the parent. If the child crashed due to IOException, then an IOException gets propagated up. Your example outputs IOException because it is the first exception with which the children coroutine has crashed.

Does it help?

@serddmit
Copy link
Author

serddmit commented Apr 5, 2021

Yes, it's helpful, but what means unwrapped?Does it relate to CancellationException or any throwable?
Anyway, thanks for the help!

@serddmit
Copy link
Author

serddmit commented Apr 5, 2021

How CancellationException is propagating up in the hierarchy and is it just ignored by cancelation exception handler?What about cause of this exception?Could it be used as a suppressed exception for the first "caught" exception?
Thanks in advance

@elizarov
Copy link
Contributor

elizarov commented Apr 6, 2021

Here "unwrapped" means that the propagated cause is not wrapped into a kind of CancellationException.

@serddmit serddmit closed this as completed Apr 6, 2021
@serddmit
Copy link
Author

serddmit commented Apr 6, 2021

Maybe I'm stupid, but I still have a question:

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception")    #7
}
val job = GlobalScope.launch(handler) {    #4
    val inner = launch { // all this stack of coroutines will get cancelled   #3
        launch {                   #2
            launch {
                throw IOException() // the original exception    #1
            } 
        }
    }
    try {
        inner.join()       #5
    } catch (e: CancellationException) {
        println("Rethrowing CancellationException with original cause")
        throw e // cancellation exception is rethrown, yet the original IOException gets to the handler    #6
    } 
}
job.join()

*# 1. Exception thrown, cancelling coroutine 1 and propagate to the parent 2
*# 2. Cancelling coroutine 2 and propagation to coroutine 3
*# 3. Cancelling coroutine 3 and propagation to coroutine 4
*# 4. Cancelling itself (with exception IOException) and waiting for the completion
*# 5. join throw CancecllationException with cause IOException
*# 6. CancecllationException with cause IOException is catched and re-thrown to parent 4 (it's CancecllationException - so it's ignored by parent) and we are still cancelling with exception from 4
*# 7. It's uncaught exception for root coroutine that would be handled by CoroutineExceptionHandler

What is unwrapped in this case and what role CancellationException with cause IOException took in this situation, I still couldn't get it?
Thanks

@elizarov
Copy link
Contributor

elizarov commented Apr 7, 2021

The CoroutineExceptionHandler #7 that is installed at the parent coroutine #4 will see and will print "unwrapped" IOExcepiton. That is what it means that exception is propagated to the parent unwrapped. The propagation up to the parent does not wrap exceptions.

However, whenever you join (like in #5) at any level of the hierarchy, you'll get an exception wrapped into a CancellationException.

@serddmit
Copy link
Author

serddmit commented Apr 7, 2021

Thank you!

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

2 participants