SE-0296: async/await

Agree, that’s why I wasn’t originally planning to bring up cancellation. I jumped in when it seemed like those details were influencing this review. As long as the door is kept open to changes around cancellation pending the other reviews I’m happy with this proposal as it is.


If @asyncHandler is more optimizable than Task.runDetached, and it still allows async code to be called from a sync context, we might expect people to include a function like this in their projects:

func asynchronous(_ f: () async -> ()) {
  await f()

Heck, even if there’s no difference in optimization, people might write that anyway because it reads better than Task.runDetached at the use-site.

That way they can write code like this:

func foo(url: URL) {
  // Do some synchronous stuff here
  print("above async block")
  asynchronous {
    let data = await download(url)
    print("download complete")
    myView.image = Image(decoding: data)
  print("below async block")
  // Do some more stuff

That’s kind of how I would expect to call async code from a sync context in the first place. I shouldn’t need to invoke anything Task related unless I want to access the task itself, eg. for cancellation or monitoring progress.

1 Like

Can you elaborate more on this? What would be the problem with every try/throw/return being a suspension point when there's an active async defer? Does this violate some invariants that the compiler team hopes to maintain, or is there a performance issue of some kind?

Other than the implementation complexities I'm just not seeing any obvious reason why this would be a strong reason not to support it. Maybe the reasons are just less obvious, but that's all the more reason to explain them clearly.

My take is that it adds complexity for the author and subsequent readers of code.

A small change in a defer block suddenly turns a whole lot of other keywords into suspension points.

That alone seems like it would be hard to grasp and could lead to bugs and oversights.

1 Like

I'm not convinced that's a real problem. These are places where the control flow is already jumping elsewhere. They wouldn't immediately suspend. They would just jump to an existing defer block, which would follow the same rules as any other block of code, where suspension points are clearly marked with await. I can't think of a reason why someone would need to know that a try/throw/return statement might lead to a suspension in a defer block.

Can you give an example of where that would be confusing or misleading? Where someone might have a bug because they didn't understand what would happen?

1 Like

If you're taking the behavior of @asyncHandler, the asynchronous block would run concurrently with the "below async block" code. For me, that's a strong reason not to introduce something like the asynchronous block, because it subverts the expectations set up by Structured Concurrency, which let you assume that the code executes linearly.

We're going into a lot of detail for an asyncHandler pitch that's not-yet-written. If it were written up in anything other than the prototype implementation, it'd say that (for example) @asyncHandler functions cannot return a value, cannot have inout parameters, and cannot throw, because all of their work happens on a separate task and you don't get to see or wait for that task.



This is my general feeling, but I don't feel strongly about it. Should we mark such defer blocks as async like we have to for functions, e.g.,

defer async {
  await fileHandle.close()

Okay, here's an example that feels a little contrived, but perhaps is indicative of more-likely problems:

let lock = mutex.lock()
defer {

// The contrived part is what you would do in here
defer {
  await fileHandle.close()

return  // implicitly suspends while closing the file handle, so the unlock might happen on a different thread


To me that implies that any subsequent statement that would exit scope and trigger that defer would have to be called with await. Thus those hidden suspensions points are no longer hidden. I think it might not be a bad idea.



That’s exactly what I want: the body of asynchronous is executed asynchronously.

Heh, it’s interesting that we see the same code, share the same understanding of how it works, and draw such different conclusions. I think code using the asynchronous function is highly readable, and straightforward to understand.

Now, I’m not currently advocating that a function like asynchronous should be included in the standard library, but I find the code using it to be much nicer than the (otherwise identical) code using Task.runDetached.

• • •

Personally, if I were designing an async system, I would probably start by seeing if there’s a workable model where sync and async functions can both call each other directly.

The basic story would be that the result of an async function needs to be awaited before use, and a sync function cannot use await.

So you can call all the async functions you like from synchronous code, but you can never use their return values. They just do stuff in the background.

However, if async let were a first-class construct, then the result of an async call could be stored as such and passed into another async function which could then await it.

In my mind, that would greatly simplify the mental model: an async function is always asynchronous; you can call it from anywhere, without any ceremony; and if you want its return value then you need to await it.

I don’t know enough about the implementation side of things to say whether that can be made into a viable and efficient system, but from a naive perspective it’s how I would want to think about asynchronous functions.


It's worth noting that what you apparently mean by "asynchronously" is what this set of evolution proposals actually calls "concurrently".

In this proposal's usage, anything that's not synchronous is asynchronous, but not everything that's asynchronous is concurrent. In fact, specifically and importantly, this particular proposal (SE-0296) is only about non-concurrent asynchronicity.

Regarding @asyncHandler, I think what we really want is sequential asynchronicity, not concurrency, though — since there is no actual design yet — it's a bit early to have that discussion (and it's not really for this forum thread).

What I mean is that an @asyncHandler @IBAction function (for example) likely needs to run its body sequentially on the main thread. For this to be asynchronous, this means "return now but run this code later", i.e. something that behaves similarly to DispatchQueue.main.async.


Is there a reference which defines these (and other related terms) as they are being used in the proposals?

If it's down to the naming of Task.runDetached, let's please talk about this in the discussion of the task library.

That's what this proposal does.

That's effectively the semantics of Task.runDetached, but you want a non-awaited call to an async function to behave that way? Again, that breaks the mental model of structured concurrency. Consider:

func f() async { }
func g() async { }

func h() async {
  await g()

In the current proposal, the call to f() is an error. If calls to asynchronous functions without await became detached tasks, then this little error of omission with await now means that f and g are running asynchronously when you didn't mean them to.

That's a future. Task.runDetached returns one (called Task.Handle, subject to design discussion elsewhere). async let is intentionally hiding the future to keep the model simpler. async let is part of Structured Concurrency, not this proposal.



The nearest to a definition that I could see was the last paragraph of the introduction:

This proposal defines the semantics of asynchronous functions. However, it does not provide concurrency: that is covered by a separate proposal to introduce structured concurrency, which associates asynchronous functions with concurrently-executing tasks and provides APIs for creating, querying, and cancelling tasks.

1 Like

Right, but the proposals are essentially going to be accepted together or redesigned together.

In order to review this proposal (which I have not yet done), I need to understand how the features of this proposal are intended to be used.

I do not want to end up in a place where the community has accepted some of the proposals, then during review of a later proposal decides that a substantially different design would be preferable, requiring that we either revisit an accepted proposal, or settle for a model that we find inferior because we refuse to do that revisiting.

Right now people are saying, “That use-case is part of a different proposal which has not yet been reviewed.”

But if we accept this proposal and then move on to reviewing the others, when questions come up about the overall design people will say, “The basic behavior has already been accepted and is no longer under review.”

In order to review the basic behavior and decide if it’s what we want, it is necessary to understand and consider the full model that is being proposed.

I've already answered this meta-comment. That said, we don't need the details of async let to address your idea, nor do we need the exact spelling of Task.runDetached to be nailed down, which is why my I responded the way I did.

As I understand it, your idea boils down to allowing synchronous code to call an async function directly, and giving it "detached task" semantics. I replied why I consider that a poor design choice, but didn't receive a technical response. If you want to argue for your idea, you can and should address that technical objection.


1 Like

I feel like that example is more of an example of "code not to put in an async function period". It would be just as dangerous to write this:

let lock = mutex.lock()
defer {

// ...

await someFunc()

Since the await happens possibly many lines removed from the defer block it would be difficult to ensure that the defer happens on the thread/queue you expect it to run. I think people who have such APIs are going to have to rethink their structure to work well in async code, and I don't feel like an async defer block changes that significantly.

Oh please no...that would be terrible UX. I believe conceptually a developer does not need to know that a try/throw/return statement might be a suspension point because the actual suspension point is in the defer block itself. The try/throw/return statement is control flow, and thus it results in jumping to some other place. That other place may happen to suspend, but that happens after the normal control flow.

If you required await on every one of those statements it would be very confusing, and introducing a single await in some far-removed code could affect many lines. I would almost rather not have the feature. But I still think await in defer can work, and I still think there's not quite a strong enough reason (that I've seen) that it shouldn't work.

1 Like

I’m not so much arguing for a particular design, as I am exploring the space of possible designs. Whereas the proposal authors have already spent significant time considering these things, others (such as myself) have not.

It is not enough just to understand the proposal: in order to express meaningful support for it, I need a solid grasp of what the alternatives are.

One is as I described above.

Another would be to say that calling an async function is always implicitly awaited (so no await at the call-site), and you must explicitly write async at the call-site to get the detached behavior. That way concurrency locations are marked with async, and otherwise code runs sequentially.

In that model, async let works as proposed, but if you don’t want the return value (or there isn’t any) you could just write async foo() (which would be equivalent to async let _ = foo() as proposed).

This would still let sync functions call async ones directly, with the only ceremony being the presence of async before the call.

The proposal provides fairly significant rationale for needing await, so if you want to pursue this design, you're going to need to:

  • Argue that await is unnecessary boilerplate that should be removed
  • Show code examples where the code gets clearer/easier to reason about because of this change

Personally, I don't think this is a good technical direction. You're syntax-optimizing the least-important case (creating a detached task) at the cost of hiding potential suspension points. It's also a fairly significant divergence from the async/await designs of effectively every other language out there, so it's going to need some strong rationale.



I said it was contrived ;)

I agree that I wouldn't want this to force us to have await return, await try, await throw, etc.

There's no technical reason that I can see, either. There are use cases (e.g., closing a file handle), and the reasons I can come up to not allow it---somewhat less-obvious potential suspension points (although there's still clearly an await in the source within the defer) and asymmetry with throw (which is banned in defer for legitimate reasons)---aren't very strong.

So let's chalk this up to "we should remove this restriction unless more arguments come up in favor of keeping it."



Again, I’m not pursuing any particular design (whereas the proposals are doing so).

I am trying to understand what alternative designs are possible, so that I can compare them with the proposed design.

If the proposal authors have strong reasons for preferring the proposed design over the alternatives (which one assumes they do), it would be useful to document those reasons in the proposal.

The “alternatives considered” section of the proposal at hand does not list any of these alternatives that we’re discussing. This makes it difficult for reviewers to understand why the specifics of the proposed design were selected over any given alternative.

I am confused by this.

The motivation section of the current proposal is dominated by (in fact I believe it consists exclusively of) discussing ergonomic problems with completion handlers. Async/await is presented as a clean replacement for completion handlers.

But completion handlers are (almost by definition) run in a “detached” manner. Some code calls a function which takes a completion handler, and then continues along sequentially. The function that took the handler, meanwhile, does its thing in the background then calls the completion handler later.

The entire purpose of functions that take a completion handler is to enable “detached” or concurrent operation.

It seems strange to propose a replacement for completion handlers, which does not solve the basic problem that completion handlers are designed for, namely running a detached task.

And it seems even stranger to categorize creating a detached task as “the least-important case”.

From my reading of the proposal, its entire motivation is to replace completion handlers, and the primary (sole?) purpose of functions that use a completion handler is to perform a detached task.

• • •

Furthermore, the alternative from my last post is syntax-optimized for the “await” case. The “detached” case requires an explicit “async” notation in that model.