Skip to content

Commit 4467c59

Browse files
Jumhynhborla
andauthored
Add defer async proposal (#2943)
* Add defer async proposal * Update proposal in response to pitch discussion * Assign SE-0493 to async defer and schedule for review. --------- Co-authored-by: Holly Borla <[email protected]>
1 parent 2918f1a commit 4467c59

File tree

1 file changed

+132
-0
lines changed

1 file changed

+132
-0
lines changed

proposals/0493-defer-async.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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

Comments
 (0)