SE-0296: async/await

Indeed, thank you! I fixed it in the original post.

Sorry, my point was just that "the proposal doesn't include cancellation as a feature of async/await" is the answer to the questions about cancellation. Those other pitches are related in that they provide cancellation features, but the async/await proposal here stands on its own without cancellation. The other pitches don't change the fact that cancellation isn't part of async/await. So unless someone thinks that cancellation must work in a way that directly modifies what is proposed here then it's ok to just stop at "cancellation isn't part of the proposed behavior of async/await". After all, if cancellation isn't handled by async/await there are likely many other possible ways of handling it, and those discussions would be appropriate in threads about those specific ideas. It's only relevant here if you think it must interact with async/await directly.

One thing I just thought of regarding cancelation in async/await and the entire concurrency roadmap is:
The implementation splits the function into partial tasks which will be run by an executor.
What happens when an executor wants to deinitialize a partial task without running it?
It cannot cancel it if cancelation is not part of async/await, so what happens?

1 Like

Special functions like deinit […] cannot be async .

A potential suspension point must not occur within a defer block.

How would we perform asynchronous cleanup if we cannot await within deinit/defer? For example, we might want to delete a file, which is an asynchronous task.

Seems like we can use runDetached (from the Structured Concurrency proposal) to start the asynchronous task but there would be no way to await the task? I think awaiting the cleanup task is important to establish back pressure so that we don’t potentially have an endlessly growing list of cleanup tasks if the cleanup tasks end up being more expensive than the actual work.

How would one implement progress reporting for an async function given that the async function cannot return anything either via return value or via inout parameter before completion of the asynchronous task?

Would wrapping the async function in a class with a progress property be the only way? That seems problematic since there could be multiple progress-reporting async functions in a class and a progress-reporting async function could be called multiple times.

I think I answered my own question. We could use runDetached (from the Structured Concurrency proposal) to get a task handle and then access the progress from the task’s local storage. Since every async function call has its own task, each async function call would be able to report progress independently.

Is that what the Swift Concurrency Roadmap authors had in mind?

+1 minus some minor points



With the current proposal, one thing that seems to be missing for me is the ability to start an asynchronous function and await it later, which is possible in other languages, like JS.

This would enable something like

let x = asyncLongRunningTask()
let y = anotherAsyncFunction()
let z = await x;
let w = await y;

For me this would be desired, as it would enable expressions similar to JS' Promise.all:

let tasks = { x in asyncMapper(x) }
let results = await Promise.all(tasks)

Because of that, I feel like a Promise type should be considered. Instead of async methods being implemented purely in the language, there should be an underlying type that enables this behavior. I would consider optionals as a precedent for this, which have a deep language integration but also can still be accessed through the Optional<Wrapped> type and created using Optional.some or Optional.none.

In that case, an async function would simply be syntactic sugar:

func foo() async -> Int { ... }

Would map to

func foo() -> Promise<Int, Never> { ... }

The concrete details of this Promise type would then be covered by the structured concurrency proposal.

Additionally, this would allow the user to call asynchronous code from synchronous contexts (and attach a completion handler if desired using something like .finally)

Quick reading

1 Like

That is in the structured concurrency proposal, with the spelling async let.


This is where the desire for "asynchronous handles" (@asyncHandler) comes from. It still needs a proper pitch---and probably a better name---but the idea is that it captures the notion of a function that is called synchronously (say, from a UI) but its body is an async task. So in your button-click example you might do something like this:

func onButtonClick(button: Int) {
  let data = await download(url: url)
  myView.image = Image(decoding: data)

with is mostly (more-optimizable) syntactic sugar for:

func onButtonClick(button: Int) {
  Task.runDetached {
    let data = await download(url: url)
    myView.image = Image(decoding: data)



Yes, this is a fair point. The Core Team has demonstrated before that they're willing to go back and fix mistakes in proposals if they come to light later on (we had a couple of those in the Swift 5.3 time-frame). If we were to accept async/await under the current set of assumptions about cancellation, then decide on a very different cancellation design as part of Structured Concurrency that requires us to rework part of async/await, we would.

... but even if that happens, I still consider it valuable to have settled the primary details of async/await before turning our energy entirely over to Structured Concurrency.



Yes, the overloading based on whether the context is async ensures that you'll get the existing request(_:) within existing non-async code and the new request(_:) within new async code, even if there's no contextual type information.



runDetached is probably your only option if you need asynchronous cleanups.

Yes, that's a reasonable approach.


1 Like

What do you think about my concern about back pressure? If that is an important (or valid) concern, should we try to change the proposal to allow await inside deinit/defer?

Banning await inside defer seems like an artificial restriction, so it seems easy to lift?

Could await inside deinit somehow be achieved by forcing functions that (implicitly) call the async deinit of an object to be async?

I’m also curious what the rationale is for disallowing await in a defer block. It wasn’t explained in the proposal, and it doesn’t seem obvious.

The rationale for deinit and property getter/setters makes sense to me, but the defer part doesn’t.

You can always refactor away the need for a defer if you were to actually hit a case where this is importer.

Perhaps, but I don't think it fits well with the goals of the proposal. It means having potential suspension points that are somewhat arbitrary, e.g., at every try and return where there is such a defer, you would have an implicit potential suspension point.

You would have a type that can only ever be inside an async, never put into any type with a non-async deinit (e.g., you couldn't create an array of such types). No, this isn't a restriction that can be lifted: it is fundamental to the model.


What do you mean by “arbitrary”?

defer is conceptually just code that runs at the end of the function. I see why it has special behavior with regards to try: we want to avoid nested error throwing. I don’t see why it should have special behavior with regards to await.

It turns any try, throw, or return when there is an active async defer into an implicit “await”. That seems reason enough not to support it.


1 Like

Ah, I see what you are saying. That makes sense. Thanks for answering all my questions!

+1 on the proposal.

While I generally like this [meaning the proposal] approach (coming from C# normally), I also wonder about how a few workflows would be implemented. Sure, Promise.all (or Task.WhenAll in C#) can be done with a loop of awaits, but what about:

let x = asyncLongRunningTask()
let y = anotherAsyncFunction()
// wait for any of x, y with a timeout.
// return any task/promise that completed.
let z = Task.any(x, y)

This is something I used several times in my C# coding, at least, usually for graceful shutdowns and the like. But it doesn't seem easy to implement this with this proposal, unless I'm missing something.

1 Like

Correct, such operations are not possible to implement with just this proposal.

These operations are enabled by TaskGroups which are part of the structured concurrency proposal - please check the evolution pitch about structured concurrency (sorry for short reply, on phone)

Okey got my hands on a laptop...

Implementing any would be done as:

Task.withGroup(resultType: T.self) { group in 
  let x = await group.add { await asyncLongRunningTask() }
  let y = await group.add { await anotherAsyncFunction() }
  return await try // returns whichever completed "first"

Note though that this is NOT part of the async/await proposal and will be proposed as part of the Structured Concurrency proposal;

...and that task groups are a low-level building block, they’re pretty verbose but are the only safe way to achieve such dynamism, we could offer helpers for awaiting on any first completed task handle etc.