Audit AI-Generated Swift for Task.detached
See Task.detached in AI-generated Swift? Stop and check it. A detached task without a cancellation story, an explicit priority, or a clear owner is a documented Swift Concurrency red flag — and coding assistants reach for it far more often than the language recommends. Most of the time you wanted a plain Task.
What Task.detached actually drops
A plain unstructured Task { ... } inherits three things from the context that launched it. Task.detached { ... } inherits none of them:
| Property | Plain Task | Task.detached |
|---|---|---|
| Task priority | Inherited from the current task | Runs at default; must be set explicitly |
| Task-local values | Inherited | Lost |
| Actor isolation | Inherited from the enclosing context | None — runs non-isolated |
Cancellation still works on a detached task if you hold the handle and call .cancel() — but a detached task is not part of the surrounding task tree, so it is not cancelled automatically when its parent is. That's the trap: fire a Task.detached off a view that disappears, and the work keeps running with nobody to stop it.
Why the assistant reaches for it
Detached tasks are the blunt way to escape an actor. When an assistant is inside a @MainActor type and wants to do background work, Task.detached compiles and "just runs off the main thread" — so it looks correct. But you usually don't want to sever isolation wholesale; you want the specific heavy call to run elsewhere, and the result to hop back to the main actor for the UI update. A plain Task plus await on an actor method does exactly that, keeps cancellation wired into the tree, and keeps your UI mutations on @MainActor.
The audit checklist
When you spot Task.detached in generated code, ask:
- Is priority set on purpose? If not, it's running at the default, not the caller's — often a subtle slowdown.
- Who cancels it? If nothing holds the handle, the work can outlive its reason to exist. Structured concurrency (
async let, task groups) or a plainTasktied to a lifecycle is usually better. - Does it touch shared mutable state or UI? If yes, it should be actor-isolated — let an actor own the state and keep UI work on
@MainActor, rather than detaching and reaching back in.
If all three answers are "no reason," rewrite it as a plain Task.
Power move: let the actor do the isolation
// Assistant output — severs isolation for no reason
Task.detached {
let data = try await loadHeavyThing()
await MainActor.run { self.items = data } // manual hop back
}
// Audited — plain Task inherits context; the actor owns the work
Task {
let data = try await repository.loadHeavyThing() // repository is an actor
self.items = data // already on @MainActor
}
The plain Task inherits @MainActor isolation from the enclosing type, so the self.items assignment is safe with no MainActor.run dance. The heavy work is isolated inside the repository actor, off the main thread, and cancellation propagates with the enclosing lifecycle. Reserve Task.detached for the rare case where you genuinely need to run independent of the current context — and when you do, set the priority and hold the handle.
Resources
- What's the difference between a task and a detached task? — Hacking with Swift
- Understanding unstructured and detached tasks in Swift — Donny Wals
- Problematic Swift Concurrency Patterns — Matt Massicotte
- Task — Swift Standard Library (Apple Developer)
Shipping Swift with an AI assistant? Yeda AI audits AI-generated code for the patterns that pass review but bite in production.