Async builder and cancellation in structured concurrency
See original GitHub issueBackground
After async
integration with structured concurrency (#552), any failure in async
cancels its parent.
For example, following code
try {
async { error("") }.await()
} catch (e: Throwable) {
...
}
cancels outer scope and it usually leads to undesired consequences. The rationale behind this change was simple, it was previously hard or impossible to have proper parallel decomposition to fail when one of the decomposed computations failed.
While there is nothing wrong with that reasoning (=> we won’t revert this change), we now have a more serious problem with user’s intuition of this behaviour.
async
is a way too polluted keyword which neither reflects its purpose in kotlinx.coroutines
nor matches similar concepts in other paradigms or programming languages, so any newcomer will be confused with this behaviour and thus has no or incorrect mental model about it.
Moreover, if someone already understands concept of kotlinx.coroutines
async, (s)he still may want to have future-like behaviour (and according to our slack, demand for that is quite high).
And there is no simple answer to that.
To have a proper semantic (one-way cancellation), one should write something like async(SupervisorJob(coroutineContext[Job])) { ... }
(really?!) and it completely defies the goal of having clear and easily understandable API. coroutineScope
is not applicable for that purpose, because it awaits all its children and GlobalScope
is just unsafe.
We should address this issue and educating users with “this is intended ‘async’ behaviour” without providing an alternative is just wrong. I can’t imagine the situation where someone asks about this behaviour in Slack and community responds with “yes, this is how it works, you actually need async(SupervisorJob(coroutineContext[Job])) { ... }
”
Possible solutions
In my opinion, it is necessary to provide future-like builder (aka “old” async) and it would be nice if its name won’t clash with anything we already have.
For example, deferred
builder. It is not something newcomers will start to use immediately (while async
is kinda red flag “please use me”) due to its name, but it is a simple concept, it is clean, short and easy to explain (see my “can’t imagine the situation” rant).
Another possible solution (please take it with a grain of salt, it requires a lot more design/discussing with other users) is to deprecate async
at all. As I have mentioned, this name is useless, polluted and does not reflect its semantics. Even with deferred
builder, we still should make a tremendous effort with educating users that this is not async
you are familiar with, this is completely different async
.
But if we will deprecate it and introduce another primitive with a more intuitive name, for example, decompose {}
(naming here is a crucial part, decompose
is just the first thing that popped in my mind), then we will have no problems with async
. Newcomers won’t see their familiar async
, but deferred
and decompose
and then will choose the right primitive consciously.
Reported: https://discuss.kotlinlang.org/t/caught-exceptions-are-still-propagated-to-the-thread-uncaughtexceptionhandler/10170 #753 #787 #691
Issue Analytics
- State:
- Created 5 years ago
- Reactions:43
- Comments:47 (31 by maintainers)
Top GitHub Comments
This is so confusing. I am reading and testing Kotlin async exception handling for days now. And I am still not getting it. This is even worse than Java with it’s Future-API from hell (can’t believe, I’m writing this).
Wish we could just have simple async/await equivalents like in C# or Javascript. They just do their stuff like expected without having to deal with (global) scopes, coroutine builders, dispatchers, suspend functions, supervisors, catched exceptions bubbling up and crashing my app etc.
The current state is just - awful. Everyone is just confused how to use all those stuff correctly. Sorry. In C# all those works with async/await like expected, done.
In Kotlin it’s rocket science.
@fvasco I don’t agree with making
launch
top level again without scope, there’s a reason it moved to structured concurrency with mandatory scopes. You can still write aglobalLaunch
or alike method if you want, but scopes are really useful forlaunch
, think Android apps, child coroutines that should not be forgotten, etc…For an
async
alternative though, likefork
or something, it would be great. Then, we could have anasync
or alike that uses a SupervisorJobso it doesn't cancel parent on failure, but only throws when
await()` is called.