Why is await unsupported in a non-async function with Void return type?

I didn't follow why the above example in WWDC21: Explore structured concurrency in swift (at 20:00) is disallowed, and you have to manually wrap the await code in an async { } block.

Since func collectionView(_:willDisplay:forItemAt:) returns no value, couldn't the runtime return control when it hits the await, and then resume again on the main thread since it's marked with @MainActor? What does an explicit async do here that the compiler couldn't already infer?

Apparently … you can’t call “await” outside of a @Sendable closure. (Or outside of another async method).

async {
}

Every sendable closure is actually owned by a Task when it’s running. And every “await” inside that Task creates a child task. This task tree is used to make error handling and “throwing” inside a concurrent code work automatically, with unfinished child tasks automatically canceled on errors.

Also the compiler will enforce that shared reference objects use the Sendable protocol (like actors) so that the compiler can prevent incorrect mutation of shared references.

Hence in swift 5.5 … code “inside” async is being compiled and executed differently than code outside (legacy swift code).

There a lot more going on inside that “async{}” notation that it seems.

We had experimented with a design where void-returning non-async functions could await async calls if they were marked with the @asyncHandler attribute, and they would behave as you described. But in adopter feedback, we found that developers found it hard to reason about exactly what part of their code would run at what point, especially if there was conditional control flow or loops involved. There are many places in AppKit and UIKit where it is very important to know what part of the code runs immediately before returning control back to the framework. So we instead designed the async API as a way of explicitly launching a task that's still related to its enclosing context. This is both clearer, because it makes the part of the code that runs later explicit, and more flexible, because you can use async anywhere in your code, including in sync functions that return an interesting value, not just Void.

8 Likes

Perhaps the example here is too simple to see where the flow would be confusing, since with the await there I would expect it to behave just as if this func were marked async, and am more confused about the need for this wrapper.

Terms of Service

Privacy Policy

Cookie Policy