SE-0296: async/await

Perhaps I'm mistaken, but in my understanding (at least conceptually), asyncFunction() does not throw until it returns (either normally or with a throw), so you don't have anything to try until you have its output in a synchronous context (i.e. after awaiting).

In my mind, in order to apply try before await, try would have to be applicable to this asynchronous promise of a result rather than just a synchronous result (as it is now).

That might be correct enough in your mental model, but the syntax of both await and try is that they just need to be written "to the left of" an expression that contains throwing or suspending sub-expressions. They're not operators applied to the sub-expressions, and there may be multiple sub-expressions of each kind, yet the await or try keyword only needs to appear once.

For that reason, await try and try await (if both were allowed, as they were in the original version of this proposal) mean exactly the same thing to the compiler.

IIUC, the point of fixing the order as await try was to avoid a debate about whether linters should enforce one order or another.

If your argument is that allowing both orders allows you to better conceptualize, then you can certainly argue that the forced ordering should be re-relaxed in the final proposal, but keep in mind that the compiler itself doesn't really care about the order.

All right, that makes sense. I do think an order should be enforced, but I don't think the proposal is enforcing the right order. Even if it's just within my conceptual model and not actually how the compiler handles it, try await makes more sense to me for reasons outlined earlier, and I'd be curious about other people's mental models justifying the opposite. As said in my initial comment, I don't think consistency with async throws beats out this (albeit conceptual) associativity.

5 Likes

I agree that the order of await try should be reversed. try await someActivity() reads naturally as “try to await someActivity,“ while “await to try someActivity“ makes less sense IMO.

Regardless, I think this proposal and structured concurrency are fantastic proposals, and I have no real complaints. I’ve used async/await in TypeScript for ages, and I believe the proposed solution is even better in several ways.

Additionally, I strongly support the proposed direction of only allowing async functions to be called from another async function, with the only entry point being the separately-proposed Task.runDetached. This makes the behavior much more explicit and prevents the “dangling promises” issue seen in JavaScript. It also makes the language easier to understand because it doesn’t break the intuition that a function call runs the entire callee before returning to the caller. This is also something that the structured concurrency proposal handles very well, without sacrificing the possibility of pseudo-parallelism.

2 Likes

An async call and return should be at least as fast as returning into an escaping callback, and we expect it to be significantly faster. It will be less efficient than a normal call and return.

7 Likes

My overall evaluation of the proposal is positive. The problem is significant and the proposal fits well with the direction of Swift. I have used async/await in other languages, although sparingly, and this proposal adds something comparable to those. I have followed the proposal from its initial phases and have reviewed this iteration in detail.

I would make two points here, which are really in the edges of the feature:

  1. Based on the discussions outlined in AsyncSequence, it would seem to me beneficial to permit get-only async properties and subscripts. This would more fully allow existing APIs to adopt or have counterparts that are async without disrupting existing Swift conventions as to what should be spelled as a property or subscript rather than a method. I think the alternatives, such as getXXX() functions, would be a setback for API naming consistency. Lifting this restriction does not poke any holes in the model proposed, and I do not think it would actually be any more difficult to explain than the present design—particularly since the explanation for that present design starts by conceding that get-only async properties and subscripts are theoretically sound. As in other situations seen in these forums (e.g. PATs), learners find it particularly baffling when a restriction is artificially imposed “for simplicity” but are much more apt to grasp restrictions that arise inherently due to the feature in question.

  2. For reasons that I cannot entirely articulate, I too find try await more satisfying than await try. It reminds me of the recently circulating reminder that the English language has a certain implicit order of adjectives which native speakers instinctively use and non-native speakers memorize. Perhaps there is something similar for verbs. Certainly, I more often “try to (some other verb)” than “(some other verb) to try.” Perhaps it has to do with the fact that English flows most naturally in an iambic meter, and “TRY aWAIT” fits that meter but “aWAIT TRY” leads to the juxtaposition of two stressed syllables, which is awkward to pronounce. (This order also leads to a satisfying mirroring: async throws -> ReturnValueType and let returnedValue = try await ....)

20 Likes

A huge +1. I have been using async/await in other languages, and for me it has been the single 'user experience' feature that I have missed the most when returning to Swift from writing in languages that already have an async/await implementation.

One thing that I missed from the Motivation in the proposal is the comparison with using Future-like implementations to solve this issue instead of only discussing the callback implementation. In my current Swift code I would almost always wrap existing 'completion handler' based code in something like the Future type from Combine, Single from RxSwift or a hand-rolled version of a Future.

Maybe the proposal also ought to spell out that there is no attempt to bridge async code to a type like Promise as it's done in TypeScript and Javascript. If this is your initial expectancy, then the apis for initiating and wrapping async code from the Structured concurrency proposal appears clumsy. Realizing that these building blocks are only meant as low level building blocks and should not be pervasive in standard swift code definitely helped me figuring out the intention of the feature a bit more.

Yes, I believe that Swift will become even more user-friendly, easy to use and easy to understand from having async/await.

Although I do remember my initial confusion many years ago when meeting the feature for the first time in C# and while debugging, realising that state was changed when I 'stepped over' a line of code with await in the debugger. I don't know if the developer tools could help people new to the concept figuring out what is going on somehow.

Yes, as mentioned, I have been missing this feature a lot.

When reading across the other concurrency proposals, and considering async let (which I guess can be compared to a Promise/Future and allows you to map and flatMap without needing to know those terms), then I believe that Swift's implementation will be stronger than what I have seen in other languages.

As others have also said - probably too much. :-) I tried to learn as much as possible from the original Concurrency manifesto, read the pitches, followed the discussions, read the final proposal and followed the discussions in here as well. I understand the intentions very well - and have a vague feeling about how the implementation with the state machine transformation and coroutine support likely works.

Regarding the discussion in https://forums.swift.org/t/on-the-proliferation-of-try-and-soon-await/42621/100, I am all for the explicit await. I don't think that the explicit try in the error handling model is an issue - on the contrary I agree that it is good for readability and understandibility. I expect my feelings about await to be the same, and I haven't found explicit await to be an issue in other languages.

4 Likes

// meh, sorry I think this was already answered :slight_smile: Going through notifications of this mega-thread and I didn't notice this was a pretty old comment.

It seems it's not called out in the proposal, but that is how it works:

 warning: no calls to 'async' functions occur within 'await' expression
        await print("")

so just a small adjustment to the proposal to call it out if you'd want to.

(recent main branch)

That’s not the same thing. Using await on an expression that doesn’t have any async calls is a warning. Is it also a warning to write a function marked as async that doesn’t include any usages of await?

2 Likes

Ah the other way around, I didn’t spot that at first read. That’s not a warning today AFAIK.

Though some functions may need to / want to be async just to future-proof “we know we’ll do some async stuff here soon” before locking in APIs...? I guess the error supression would be “call some await asyncNoop()”... perhaps a bit weird? No idea what Doug thinks about this tho :slight_smile:

1 Like

They aren't on the normal stack. async functions record their local variables on a separate stack, which is allocated in the task itself. Before an async function hits a potential suspension point, it writes any values that it will need on resumption into that separate stack so they can be accessed by the resumption code.

Now, if a particular local variable won't be access across a potential suspension point, it will go onto the normal stack. The same thing happens for any synchronous code you call from within your async function.

Doug

2 Likes

@Douglas_Gregor thanks, that explains a lot. Essentially it's like closures (with no way to weak-ify any of the captured vars though?)

Edit: of course you can weak-ify anything you want by renaming, i.e. weak var weakSelf = ... before the async call.

Great response. It seems there are very few references to Combine here, so I’ll comment on your review. If this is off-topic then I apologize and will take the discussion to a more appropriate place.
My question is, when would you reach for async/await instead of using Combine?
Or is async/await just something that one would use to implement a library like Combine? (I.e. async/await is more low-level than Combine)

I think that your question is perfectly appropriate to ask here. :-)

async/await is definitely meant to be used 'directly' and not just as a low level construct for building other abstractions.

Combine provides generic Publishers that model streams of values over time. async/await does not provide any abstraction like that.

But specifically for the Future publisher, which only ever publishes a single value (or fails), the concept aligns perfectly with async/await.

My guess is that when async/await lands, I will very, very rarely use types like Future again.

There are of course use cases for it: for instance when you have something that you need to bridge to a Combine Publisher. In this situation I'm sure that Combine will provide an easy way to wrap async code in a Future and perhaps also provide an api to await the value of a Future (although this is of course just me guessing).

But for all situations where you currently have a Future and perform a bunch of chained map and flatMap steps, and in the end get the result through .sink(...), then you can basically replace it by a series of statements that 'read' like synchronous code, but is in fact asynchronous.

Similarly, any function you have that returns a Future can be easily converted into an async function.

6 Likes

What is your evaluation of the proposal?
+1, very happy this is going forward.
I just wish there was a word on non-concurrency uses of this, (eg: generators), maybe in the future direction section.
EDIT: generators are related to coroutines, but not to async/await.

Is the problem being addressed significant enough to warrant a change to Swift?
Yes, I firmly believe so

Does this proposal fit well with the feel and direction of Swift?
I does. It is focused, it prones explicitness and it addresses the problem.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
I have not.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
More on the side of a quick reading, although I also read other threads and manifestos on async/await, coroutines, futures, and structured concurrency.

About async computed properties. It would make sense, but I reckon it should be a different proposal.

About concurrency and cancellation. I think the concerns are well separated between this proposal and the Structured Concurrency proposal. This proposal doesn’t resolve those, only mention them as part of the overall concurrency roadmap, and to point at the relevant proposals.

  • What is your evaluation of the proposal?
    Strong +1

  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes, definitely

  • Does this proposal fit well with the feel and direction of Swift?
    Yes.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    async/await has proven very successful in other languages like JavaScript or C#. I feel the proposal fits perfectly with this direction.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    Done a good review of the proposal. Also installed the snapshot, wrote some code to try the proposed changes, and wrote a small guide about it, so other folks could try it too.

3 Likes

I really like it.

Absolutely.

I think so. I also think that the await keyword is essential (just as try).

And as a side note: In my opinion, await try should not be combined into a single keyword. It is essential to be able to visually scan a function for await or try respectively in order to find every point that can throw or suspend.

I haven't.

I have read all concurrency proposals and followed many of the discussions.

3 Likes

Despite being really busy, I have been eagerly (but quietly) following the concurrency story. I am glad that it is finally here.

Very enthusiastic +1. We probably could still improve the text of the proposal to make it clearer. I don't think it is underspecified, but it could be clearer about advantages of this underspecification for future optimizations. Also, the amount of confusion around this group of proposals indicate that we need a lot of documentation and examples tailored for people with different backgrounds to help ease the transition.

Absolutely. It is long overdue.

Absolutely.

I think this coroutine-based design is better than most other async/await implementations and leaves more room for optimizations and tooling support.

Read the proposal and about a third of the discussions.

1 Like

Just want to throw my hat in here and say that I concur that suspends is a clearer keyword than async for this behavior.