SE-0493: Support `async` calls in `defer` bodies

As an outsider, I notice that defer async {…} is largely rejected as a requirement because it’s not necessary in the common case of minimal defer blocks.

But perhaps it should still be permitted, and, if declared, it could lead to easier scanning and helpful warnings and errors?

It’s akin to type-inference for a local variable. In complicated cases it can be clearer to state the intention up front, even if redundant, for purposes of scanning the code. The verbosity of prefix declarations can be easier to read than the more concise but indefinite inference from scanning the whole block, particularly for complex aspects.

A defer block is unlike do, while, or if because it has this weird non-local action-at-a-distance effect that’s already hard to think about. For async calls in particular, it would be helpful to know that there’s always another suspension point for the given context. (After all, it was decided to require awaitat every possible suspension point.)

As a precedent, I’d think the same could be true for other effects: even when they could be inferred (somewhere in the nether regions of the closure…), they should be able to be stated for clarity and fo feedback when they’re not true (e.g., when other types of errors are being thrown).

But how far to take this prefix/declaration approach?

If/since async is to be inferred, how would the user declare that they intend the defer block to be synchronous in an otherwise asynchronous function? Swift doesn’t have the inverse sync declaration to state the negative (throws(Never) is nice that way).

One workaround for that would be to declare a function local to the context and call that in the defer block (converting it to the minimal case). (I’ve always assumed without investigating that the compiler would avoid a runtime function call for local functions, but I should probably check that, esp. for defer.) The same workaround applies to the declared-async case. But the ceremony seems inapt for Swift; we don’t require variables to be declared with types first in one statement before they could be used in another below.

I find myself starting to use defer a lot, not just for resources and function exits. I do more than I should of complicated parallel struct/array loops where I put defer’s for progressing the data next to each access, and it would help to have the effects declared for each.

2 Likes

Right, my point about do/if/while should only be understood as a response to the idea that code folding makes defer somehow particularly problematic. We're on the same page with respect to the 'surprising' thing about defer being that it's syntactic position in code can bear little relation to its logical position in the actual execution graph.

My instinct is still that if a defer has grown large enough that the await has moved far from the defer (such that defer async would be a meaningfully useful marker), then it is no longer particularly helpful to know that the defer suspends somewhere—it's much more important to know precisely where (as marked by await), since thats the point at which you have to think about reentrancy, invariants, etc. Moreover, it doesn't do anything to solve the real potential point of confusion that the actual suspension point in the logical flow is divorced from its syntactic position.

It is important for functions to mark themselves as async because they form an API boundary; but this doesn't apply to defer. Similarly, I don't really see the use of this:

Since defer doesn't form an API boundary, it can freely move between being synchronous and asynchronous in an async function. In what situations would a defer need to be synchronous when the surrounding context is already async. Or, put another way, if one had a defer sync and suddenly found themselves needing to do some async cleanup, what would/could they do apart from merely deleting the sync, or replicating defer by pasting await cleanup() along every exit path manually?

Lastly, to the suggestion that defer async be optional but not required, I'd just say that if we find a mere await to be confusing in practice in certain cases, which would have been improved by marking the defer itself, we could always introduce this option additively—the only thing we're more or less locking in with this proposal is that such marking would not be required.

+1, this will be an excellent addition.

Agreed that an optional async doesn’t really help much. It’s not like an async would “force” you to actually have some await inside it; it’d be like async functions which may, or may not actually await/suspend. So we’re not really gaining much strong guarantees by allowing the async just optionally.

So I think we’re back to just allowing async code in defers being the simplest and consistent thing we can do here.