From my point of view, the current async/await syntax lacks some features that throws/try has:
await!
let result = await! asyncTask()
Sometimes it’s impossible to make the calling context async, and it’s acceptable to block the current thread, similar to how semaphores work. Like try!, await! should be used with extreme caution, generally avoided when possible, and ideally flagged by linters with warnings or errors.
reasync
This has been discussed frequently on forums. What I want to add is: what if all non-escaping closures were rethrows/reasync by default? I don’t see any major issues with this, except that it introduces some implicit behavior to the language. However, in this case, I think it’s acceptable because the compiler always knows which functions can throw and enforces try where needed.
I don't think a blocking syntax is ever going to be introduced into Swift Concurrency. Because it goes against one of the core contract of this system -- definite forward progress.
try! goes against the core contract of the try/throw system in the same way, but it still exists and is useful—because the “contract” should be the primary mechanism, not the only one
I don’t think this is an accurate comparison. try! is “just” shorthand for do { try foo() } catch { fatalError() }. You can make a code style argument that you don’t want to use it in your codebase, but there’s nothing fundamentally unsound about it. await! on the other hand…
One of the ways Swift concurrency can be very efficient is that it spawns one worker thread under the hood for each CPU core you have. This allows you to run work in parallel but doesn’t require as many kernel resources (memory, CPU time) as having a separate kernel/OS thread for each task. A consequence of this model is that Swift concurrency does not use pre-emptive multitasking (where one task is forced to stop doing work on the CPU so another one can start). Instead, tasks can use the CPU for as long as they want, until they hit an await. At that point, Swift’s scheduler is able to start running another task or keep running the existing one. Since modern devices have multiple cores and async work often awaits, this works pretty well in practice.
However, one thing you must never do under this system is wait synchronously for future work that must be performed by Swift concurrency. That means that it can be OK to do things like file or network I/O synchronously (you’re just reducing the total percentage of the CPU your program can use for a little while). But something like await! is not ok. It’s fine if you use it once: one of the threads in the tread pool will be blocked for a bit, but the others are perfectly capable of making forward progress, and one of them will eventually finish the work the blocked thread is waiting on. However, if all of your threads hit an await!, there are no threads left to make forward progress and your process’s background tasks will stop responding and never recover.
If you’ve used Grand Central Dispatch, you might be surprised because this is less of an issue there. Since cases of this anti-pattern are already present, GCD will start up new OS-level threads in some cases to help un-block you here. But it has a limit too (higher than the number of CPU cores) so you can still get yourself into the same sort of trouble.
Swift concurrency was a completely new system, and the authors were able to improve efficiency by not adding this behavior of spawning new threads since there was no existing code to use the antipattern. So they wrote the rule of always requiring forward progress into the contract for work performed on the concurrent thread pool.
The issue with reasync is a lot less complicated: async functions are called differently than synchronous functions. rethrows works because throwing functions put the error in an extra register reserved for this purpose by Swift that is ignored by non-throwing functions. Since async functions have to be able to suspend, my understanding is that they are called a little bit like classic completion-handler functions (where you provide a function to run once the async work finishes). This means that a function written as reasync would need to have its code emitted twice (once for sync, once for async). That’s definitely something the compiler could do in theory, but nobody has implemented and pitched it yet.