Closed
Description
Consider following snippet:
val job = async() {
barrier.await()
throw IOException()
}
barrier.await()
job.cancel(TestException())
job.await() // <- 1
1
will always throw IOException
and it will have eitherCompletedExceptionally
or Cancelled
state. TestException
is always lost even when cancel
call was successful.
Now slightly rewrite it:
val job = async() {
barrier.await()
withContext(wrapperDispatcher(coroutineContext)) { // Just a wrapper to avoid fast paths in implementation
throw IOException()
}
}
barrier.await()
job.cancel(TestException())
job.await() // <- 2
Now 2
now can throw JobCancellationException: Job is being cancelled
which definitely shouldn't be a terminal exception. Depending on timing it also may lose either TestException
or IOException
(even when its known that IOException
was thrown).
What user may expect
- When it's known that
IOException
was thrown,await()
should throw it as well. If concurrent cancellation with the cause was successful,IOException
should have its cause as suppressed exception - When it's known that
IOException
wasn't thrown (cancellation was "first"),await
should throwTestException
JobCancellationException
should never be thrown if the cause is present on all code paths and the job is in its final state- No intermediate
JobCancellationException
should be present in the final state ofJob
We should design, rework and document exception handling mechanism.
We can give up some performance on an exceptional path (especially when multiple exceptions are thrown), but regular code path should stay the same