I have difficulties understanding what try await would mean. Do you call a function that may succeed or fail to call an async function or are you calling an async function that might fail?
Regarding context switching: one could just make this mostly impossible whatsoever in async code. If we think of async functions as functions with completion handlers that are called on return and of ``àwait```simply as a function call, the mere fact that
let x = await foo()
return await bar(x)
is written in assignment style prevents you from doing something along the lines of
DispatchQueue.main.async{
return foo() //error
}
If we do need to switch to another context, that would be done at the periphery. So, you chain a lot of async functions or combine them using some function like
//can run both closures in parallel (if a proper context is injected),
//but completion handler is once again abstracted away
// to allow more imperative looking code
zip<T,U>(_ closure1: () async -> T, _ closure2: () async -> U) async -> (T,U)
And evetually you actually run you long chain of async functions using a single global function like this:
startAsync<U>(serialContext: Context, parallelContext: Context, completionContext: Context, closure: () async -> U, completion: (U) -> Void)
The idea is that the outermost function is called using the serial context and every time the compiler hits the return keyword, the continuation will also be called on that serial context. Additionally, there needs to be some primitive to get access to startAsync's parallel context. When the compiler sees the last return, the underlying continuation will be called on startAsync's completion context. This is the point where you actually may change contexts.
From my experience, you don't do any deep nesting at this peripheral stage. Most of the time, you just put the value produced by the long async procedure back on the main queue and mainipulate the UI or the app state with it. All the deep nesting should be done by chaining async functions that will run in the same context, and if you do want to start any new async procedures at the periphery as a sideeffect, you can once again call just chained processes and avoid all deep nesting.
Note that this doesn't mean that I propose this syntax for starting an async process in particular, I just wanted to demonstrate that theoretically it shouldn't be toooo difficult to implement or too hard to understand. Also note that
let x = await foo()
let y = await bar()
would then definitely run sequentially. However, the compiler should be able to find out that these terms don't depend on each other and warn you that the code could be optimized with zip - a warning that you should be able to suppress though, as this optimization doesn't always pay off.