Pattern
In a synchronous function, defer { x = ... } followed by Task { await ... ; ... uses x ... } is a recurring bug. The deferred change runs the moment the function returns (before the Task body has a chance to run), so the cleanup is observed inverted from intent.
Canonical example (loading-flag management in SwiftUI view models):
func login() {
isLoading = true
defer { isLoading = false }
Task {
await doSomethingAsync()
}
}
isLoading flips to false immediately when login() returns, while doSomethingAsync() is still running.
Why a rule
- The pattern is widespread in tutorials, SwiftUI sample code, and production. Folklore-known but not flagged anywhere.
- The compiler does not warn (verified against current main).
- SwiftLint already has
unhandled_throwing_task and inert_defer. This rule sits naturally between them.
- SE-0493 (async-defer) and SE-0520 (discardable Task) are the relevant Swift Evolution context, neither addresses this shape.
Proposed heuristic (narrow, opt-in)
Trigger when ALL hold:
DeferStmt inside a synchronous function or closure.
- The defer body contains one or two simple assignments to identifiers (filters out logging-only defers and lock release).
- A sibling statement at the same scope is a discarded
Task { ... } or Task.detached { ... } initializer.
- The Task's trailing closure references at least one of the identifiers assigned in the defer.
Anything outside this shape does not warn. False-negative-friendly by design.
Negative examples (must NOT warn)
func f() async { // async + await directly: correct
isLoading = true
defer { isLoading = false }
await work()
}
func f() { // logging-only defer
defer { print("done") }
Task { await work() }
}
func f() { // Task is captured
let t = Task { await work() }
defer { print("leaving") }
_ = t
}
func f() { // no shared state
var localFlag = true
defer { localFlag = false }
Task { await work() }
_ = localFlag
}
Positive examples (must warn)
func login() {
isLoading = true
defer { isLoading = false } // <- triggers
Task {
await doSomethingAsync()
_ = isLoading
}
}
func login() {
isLoading = true
defer { isLoading = false } // <- triggers
Task.detached {
await doSomethingAsync()
_ = await self.isLoading
}
}
Suggested fix-its (textual)
- Move the
defer inside the Task closure.
- Make the enclosing function
async and await the work directly.
Reference template
Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swift is structurally near-identical (recognizes Task initializers and inspects their trailing closures).
A working sketch is in this repo at swiftlint-rule/DeferBeforeUnstructuredTaskRule.swift.
Severity / opt-in
Default warning, opt-in. After adoption data accumulates, the heuristic could feed a Swift Forums pitch for a compiler-side diagnostic.
Pattern
In a synchronous function,
defer { x = ... }followed byTask { await ... ; ... uses x ... }is a recurring bug. The deferred change runs the moment the function returns (before the Task body has a chance to run), so the cleanup is observed inverted from intent.Canonical example (loading-flag management in SwiftUI view models):
isLoadingflips tofalseimmediately whenlogin()returns, whiledoSomethingAsync()is still running.Why a rule
unhandled_throwing_taskandinert_defer. This rule sits naturally between them.Proposed heuristic (narrow, opt-in)
Trigger when ALL hold:
DeferStmtinside a synchronous function or closure.Task { ... }orTask.detached { ... }initializer.Anything outside this shape does not warn. False-negative-friendly by design.
Negative examples (must NOT warn)
Positive examples (must warn)
Suggested fix-its (textual)
deferinside theTaskclosure.asyncandawaitthe work directly.Reference template
Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swiftis structurally near-identical (recognizesTaskinitializers and inspects their trailing closures).A working sketch is in this repo at
swiftlint-rule/DeferBeforeUnstructuredTaskRule.swift.Severity / opt-in
Default
warning, opt-in. After adoption data accumulates, the heuristic could feed a Swift Forums pitch for a compiler-side diagnostic.