|
| 1 | +# Support `async` calls in `defer` bodies |
| 2 | + |
| 3 | +* Proposal: [SE-0493](0493-defer-async.md) |
| 4 | +* Authors: [Freddy Kellison-Linn](https://github.com/Jumhyn) |
| 5 | +* Review Manager: [Holly Borla](https://github.com/hborla) |
| 6 | +* Status: **Active review (September 22 - October 6, 2025)** |
| 7 | +* Implementation: [swiftlang/swift#83891](https://github.com/swiftlang/swift/pull/83891) |
| 8 | +* Review: ([pitch](https://forums.swift.org/t/support-async-calls-in-defer-bodies/81790)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +This is a targeted proposal to introduce support for asynchronous calls within `defer` statements. Such calls must be marked with `await` as any other asynchronous call would be, and `defer` statements which do asynchronous work will be implicitly awaited at any relevant scope exit point. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +The `defer` statement was introduced in Swift 2 (before Swift was even open source) as the method for performing scope-based cleanup in a reliable way. Whenever a lexical scope is exited, the bodies of prior `defer` statements within that scope are executed (in reverse order, in the case of multiple `defer` statements). |
| 17 | + |
| 18 | +```swift |
| 19 | +func sendLog(_ message: String) async throws { |
| 20 | + let localLog = FileHandle("log.txt") |
| 21 | + |
| 22 | + // Will be executed even if we throw |
| 23 | + defer { localLog.close() } |
| 24 | + |
| 25 | + localLog.appendLine(message) |
| 26 | + try await sendNetworkLog(message) |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +This lets cleanup operations be syntactically colocated with the corresponding setup while also preventing the need to manually insert the cleanup along every possible exit path. |
| 31 | + |
| 32 | +While this provides a convenient and less-bug-prone way to perform important cleanup, the bodies of `defer` statements are not permitted to do any asynchronous work. If you attempt to `await` something in the body of a `defer` statement, you'll get an error even if the enclosing context is `async`: |
| 33 | + |
| 34 | +```swift |
| 35 | +func f() async { |
| 36 | + await setUp() |
| 37 | + // error: 'async' call cannot occur in a defer body |
| 38 | + defer { await performAsyncTeardown() } |
| 39 | + |
| 40 | + try doSomething() |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +If a particular operation *requires* asynchronous cleanup, then there aren't any great options today. An author can either resort to inserting the cleanup on each exit path manually (risking that they or a future editor will miss a path), or else spawn a new top-level `Task` to perform the cleanup: |
| 45 | + |
| 46 | +```swift |
| 47 | +defer { |
| 48 | + // We'll clean this up... eventually |
| 49 | + Task { await performAsyncTeardown() } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +## Proposed solution |
| 54 | + |
| 55 | +This proposal allows `await` statements to appear in `defer` bodies whenever the enclosing context is already `async`. Whenever a scope is exited, the bodies of all prior `defer` statements will be executed in reverse order of declaration, just as before. The bodies of any `defer` statements containing asynchronous work will be `await`ed, and run to completion before the function returns. |
| 56 | + |
| 57 | +Thus, the example from **Motivation** above will become valid code: |
| 58 | +```swift |
| 59 | +func f() async { |
| 60 | + await setUp() |
| 61 | + defer { await performAsyncTeardown() } // OK |
| 62 | + |
| 63 | + try doSomething() |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +## Detailed design |
| 68 | + |
| 69 | +When a `defer` statement contains asynchronous work, we will generate an implicit `await` when it is called on scope exit. See **Alternatives Considered** for further discussion. |
| 70 | + |
| 71 | +We always require that the parent context of the `defer` be explicitly or implicitly `async` in order for `defer` to contain an `await`. That is, the following is not valid: |
| 72 | + |
| 73 | +```swift |
| 74 | +func f() { |
| 75 | + // error: 'async' call in a function that does not support concurrency |
| 76 | + defer { await g() } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +In positions where `async` can be inferred, such as for the types of closures, an `await` within the body of a `defer` is sufficient to infer `async`: |
| 81 | + |
| 82 | +```swift |
| 83 | +// 'f' implicitly has type '() async -> ()' |
| 84 | +let f = { |
| 85 | + defer { await g() } |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +The body of a `defer` statement will always inherit the isolation of its enclosing scope, so an asynchronous `defer` body will never introduce *additional* suspension points beyond whatever suspension points are introduced by the functions it calls. |
| 90 | + |
| 91 | +## Source compatibility |
| 92 | + |
| 93 | +This change is additive and opt-in. Since no `defer` bodies today can do any asynchronous work, the behavior of existing code will not change. |
| 94 | + |
| 95 | +## ABI compatibility |
| 96 | + |
| 97 | +This proposal does not have any impact at the ABI level. It is purely an implementation detail. |
| 98 | + |
| 99 | +## Implications on adoption |
| 100 | + |
| 101 | +Adoping asynchronous `defer` is an implementation-level detail and does not have any implications on ABI or API stability. |
| 102 | + |
| 103 | +## Alternatives considered |
| 104 | + |
| 105 | +### Require some statement-level marking such as `defer async` |
| 106 | + |
| 107 | +We do not require any more source-level annotation besides the `await` that will appear on the actual line within the `defer` which invokes the asynchronous work. We could go further and require one to write something like: |
| 108 | +```swift |
| 109 | +defer async { |
| 110 | + await fd.close() |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +This proposal declines to introduce such requirement. Because `defer` bodies are typically small, targeted cleanup work, we do not believe that substantial clarity is gained by requiring another marker which would remain local *to the `defer`* statement itself. Moreover, the enclosing context of such `defer` statements will *already* be required to be `async`. In the case of `func` declarations, this will be explicit. In the case of closures, this may be inferred, but will be no less implicit than the inference that already happens from having an `await` in a closure body. |
| 115 | + |
| 116 | +### Require some sort of explicit `await` marking on scope exit |
| 117 | + |
| 118 | +The decision to implicltly await asyncrhonous `defer` bodies has the potential to introduce unexpected suspension points within function bodies. This proposal takes the position that the implicit suspension points introduced by asynchronous `defer` bodies is almost entirely analagous to the [analysis](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0317-async-let.md#requiring-an-awaiton-any-execution-path-that-waits-for-an-async-let) provided by the `async let` proposal. Both of these proposals would require marking every possible control flow edge which exits a scope. |
| 119 | + |
| 120 | +If anything, the analysis here is even more favorable to `defer`. In the case of `async let` it is possible to have an implicit suspension point without `await` appearing anywhere in the source—with `defer`, any suspension point within the body will be marked with `await`. |
| 121 | + |
| 122 | +### Suppress task cancellation within `defer` bodies |
| 123 | + |
| 124 | +During discussion of the behavior expected from asynchronous `defer` bodies, one point raised was whether we ought to remove the ability of code within a `defer` to observe the current task's cancellation state. Under this proposal, no such change is adopted, and if the current task is cancelled then all code called from the `defer` will observe that cancellation (via `Task.isCancelled`, `Task.checkCancellation()`, etc.) just as it would if called from within the main function body. |
| 125 | + |
| 126 | +The alternative suggestion here noted that because task cancellation can sometimes cause code to be skipped, it is not in the general case appropriate to run necessary cleanup code within an already-cancelled task. For instance, if one wishes to run some cleanup on a timeout (via `Task.sleep`) or via an HTTP request or filesystem operation, these operations could interpret running in a cancelled task as an indication that they _should not perform the requested work_. |
| 127 | + |
| 128 | +We could, instead, notionally 'un-cancel' the current task if we enter a `defer` body: all code called from within the `defer` would observe `Task.isCancelled == true`, `Task.checkCancellation()` would not throw, etc. This would allow timeouts to continue to function and ensure that any downstream cleanup would not misinterpret task cancellation as an indication that it should early-exit. |
| 129 | + |
| 130 | +This proposal does not adopt such behavior, for a combination of reasons: |
| 131 | +1. Synchronous `defer` bodies already observe cancellation 'normally', i.e., `Task.isCancelled` can be accessed within the body of a synchronous `defer`, and it will reflect the actual cancellation status of the enclosing task. While it is perhaps less likely that existing synchronous code exhibits behavior differences with respect to cancellation status, it would be undesirable if merely adding `await` in one part of a `defer` body could result in behavior changes for other, unrelated code in the same `defer` body. |
| 132 | +2. We do not want a difference in behavior that could occur merely from moving existing code into a `defer`. Existing APIs which are sensitive to cancellation must already be used with care even in straight-line code where `defer` may not be used (since cancellation can happen at any time), and this proposal takes the position that such APIs are more appropriately addressed by a general `withCancellationIgnored { ... }` feature (or similar) as discussed in the pitch thread. |
0 commit comments