Question 9
Task.detached — when it's justified
When is Task.detached the right tool, and what guarantees do you lose compared to structured Task { }?
Answer outline
One-sentence model: Task { } inherits actor isolation, priority, and cancellation from its creator. Task.detached inherits none of these — it starts fresh on the cooperative thread pool.
When to reach for it: when the calling code is @MainActor and Task { } would bind the work to the main thread. A detached task won't hop back to @MainActor automatically, so CPU-heavy work stays off main.
What you give up — and must handle yourself:
(1) Priority — not inherited; set it explicitly with priority: .userInitiated or .utility.
(2) Parent cancellation — detached tasks are not cancelled when the parent is cancelled; store the handle and call cancel() in deinit / viewWillDisappear.
(3) Actor isolation — hop back to the main actor explicitly with await MainActor.run { } for any UI update.
Default: prefer a non-main actor or a non-isolated async helper to move work off main. Reach for Task.detached only when Task { } inheritance would actively cause the wrong behaviour.
Principles
Task.detachedis a scalpel — use it whenTask { }inherits the wrong actor, not as a general 'run in background' shortcut.- Always store the handle and cancel in
deinit— detached tasks outlive their owner silently if you don't. - Cancellation is still cooperative inside a detached body — call
Task.checkCancellation()at each checkpoint.
@MainActor
class ViewModel {
func load() {
Task {
heavyCPUWork() // ❌ still on MainActor — inherited isolation
}
Task.detached(priority: .userInitiated) {
heavyCPUWork() // ✅ off main — no inherited isolation
await MainActor.run { self.updateUI() } // hop back explicitly
}
}
}
final class ThumbnailLoader {
private var task: Task<Void, Never>?
func load(url: URL, into imageView: UIImageView) {
task?.cancel()
task = Task.detached(priority: .userInitiated) { [weak imageView] in
guard !Task.isCancelled else { return }
let (data, _) = try await URLSession.shared.data(from: url)
let img = UIImage(data: data)?.preparingThumbnail(of: CGSize(width: 64, height: 64))
guard !Task.isCancelled else { return }
await MainActor.run { imageView?.image = img }
}
}
deinit { task?.cancel() }
}
Follow-up angles
- If
Task { }inheritance is correct but you want lower priority, useTask(priority: .utility)before reaching for detached — you keep structured lifetime without the manual overhead.