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

Hello, Swift community!

The review of SE-0493: Support async calls in defer bodies begins now and runs through October 6, 2025.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0493" in the subject line.

Trying it out

If you'd like to try this proposal out, you can download a toolchain supporting it for Linux, Windows, or macOS.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?

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

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

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

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

https://github.com/swiftlang/swift-evolution/blob/main/process.md

Thank you,

Holly Borla
Review Manager

28 Likes

Hurray! This came up a lot of times since 5.5, hopefully it makes it this time. A strong +1 from me.

+1 on the current version from me. As currently proposed, the defer statement is still the same as manually copying the same block of code to every exit path (including catch blocks), which doesn't affect cancellation behavior.

+1 on the proposal. While it doesn't solve the asynchronous resource management problem I think it addresses an inconsistency in the language. Regardless if developers run clean-up code normally or in a defer they will have to consider task cancellation anyways.

4 Likes

This is great! +1

I’ve needed this many times. The proposal looks great. +1!

Since it was mentioned I’ll also +1 to expressing interest on the more general withCancellationIgnored.

+ 1.

Looks good. I Agree on the reasons why cancellation is honored as normal and that a general purpose withCancellationIgnored can be part of a future proposal.

Simply +1, long awaited.

1 Like

LGTM! I saw some discussion in the pitch threads about a throwing defer as a potential future direction. Is this anything about this specific pitch that would in any way block a potential future throwing defer from shipping? Are there any gotchas or callouts that should be added to the proposal for engineers that want to come back to this in the future to ship throwing defer? Or are they independent and orthogonal such that a throwing defer is just an easy composition that can happen later without too much trouble?

How do I put this concisely and politely?

YES PLEASE. :grin:

Edit: I should probably say something substantive. The reasoning in the proposal seems perfectly sound to me. And having async defer behave fundamentally the same way as defer does today is absolutely the right way to go.

I look forward to using this feature in Swift Testing!

4 Likes

I don’t see any reason why anything in this proposal would block future work on throwing defer. I expect that whatever difficulties we’ll have to contend with will be equally applicable to both regular defer bodies in addition to defer bodies which contain an await. Open to the idea that there’s something I haven’t considered but as far as I can tell these are separable concerns!

2 Likes

The only real concern I have with throwing defer is how to handle the case where two or more errors might need to be thrown:

func f() throws {
  defer { throw DeferredError() }
  throw OriginalError()
}

Which is as much a question of documentation as it is anything else. And I don't read this question as blocking async defer.

3 Likes

Right, there's a fundamental semantic problem with throwing defer that the throw messes up whatever control flow you were doing previously to exit the defer scope. If that control flow was normal, the silent override from the error is much trickier for programmers to reason about. If it was abnormal, you have to decide which error actually gets thrown. There's just no appealing generic answer there in any case. But, fortunately, it's completely separable from async.

4 Likes

I think this is a very good idea.

While the point made in the alternatives about there being no await at the actual site of the suspension is interesting, I don’t think there’s a good design that would adequately address this issue. The only model I can think of in the language for an await happening later is with async let, but that only covers a single expression, not multiple statements, and it provides a natural syntax to hang the eventual await keyword off of. There are places where scopes end and defers may be run that simply do not have anything to mark with await, or where requiring an await would shed more heat than light (imagine if you had to await every try that could run an async defer). I think we’ll just have to consider the await being inside a defer to be notice enough that the scope exit may suspend execution.

Great to see this come to review :slight_smile:

As we chatted in the previous thread, I think this is very good and a necessary step towards better async cleanups, that we’re slowly making progress towards recently (isolated deinits, this feature, and in the future cancellation shielding, and eventually most likely some form of context managers…).

And this version has the right tradeoffs about cancellation–we can build on top of it with cancellation shielding later on, separately.

The implicit await at end of scope if unavoidable and the right thing to do anyway; It is already a thing with async let so it’s not a new surprising semantic in that sense.

–

I kept thinking about whether or not we need the async in defer async. The one problem I see with omitting the async is that technically, when folding code, like so:

func test() async { 
  defer { ... } // folded
}

then… we don’t have information by scanning just the ā€œouterā€ keywords anymore about the suspensions… if the body of the defer has an await, there will be a suspension, if not… then not.

This is in contrast to async let, where the same scope the async appears is known to potentially suspend:

func test() async { 
  async let x = { ... } // folded or just "don't have to read the body"
}

So there’s a bit of a difference here… It would be a bit more consistent to say that implicit suspensions can only happen in a scope where an async happens.

I think the explicit defer async is a small price to pay for the ability to very easily visually scanning a piece of code to understand if there might be suspensions, rather than having to then unpack and think about the defer might have an await etc.

Where this is important is of course trying to write code which doesn’t accidentally become reentrant due to such suspension.

func test() async { 
  defer async { ... } // folded or just "don't have to read the body"
}

So this is definitely simpler to read and remember and more visually consistent… so I’m leaning towards that to be honest.

If one were to think about the { } like it’s just a closure then one might argue that we just infer the throws and async on those… However, such closure isn’t called by itself – and any call site would then have the visual signal about those effects. So I think we have to base our comparison here on the existing prior art of async let and not just treating it ā€œjustā€ like a closure.

It’s not a huge argument, but thinking about this a bit today I ended up thinking that for visual consistency and ease of reading the defer async may be preferable after all.

It also makes error messages and explaining things a bit easier, since we can name it an ā€œasync deferā€ when we refer to it, but that’s just a minor bit.

3 Likes

Where this is important is of course trying to write code which doesn’t accidentally become reentrant due to such suspension.

I don’t think this is such a big problem, since you already have the async keyword in the function declaration, meaning you already need to keep a careful eye against reentrancy in this scope.

I don’t think it’s in any way different from await calls being folded out inside do { … } or if … { … } bodies or such.

1 Like