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
Task { } inherits the actor context, priority, and task-local values of the code that creates it. Task.detached inherits none of them and starts fresh on the cooperative thread pool. Both are unstructured, so neither is canceled automatically when the task that created it is canceled.
Reach for Task.detached when the calling code is @MainActor and Task { } would bind the work to the main thread. A detached task never hops back to the main actor on its own, so CPU-heavy work stays off the main thread.
Detaching means you handle three things yourself:
- 1.Priority: nothing is inherited, so set it explicitly with
priority: .userInitiatedor.utility. - 2.Actor isolation: hop back to the main actor explicitly with
await MainActor.run { }for any UI update. - 3.Lifetime: no parent cancels it for you, so store the handle and call
cancel()indeinitorviewWillDisappear.
By default, prefer a non-main actor or a non-isolated async helper to move work off the main thread. Reach for Task.detached only when the inheritance of Task { } would cause the wrong behavior.
Principles
Task.detachedis a scalpel: use it whenTask { }would inherit the wrong actor, never as a general run-in-background shortcut.- Always store the handle and cancel it in
deinit, because otherwise a detached task silently outlives its owner. - Cancellation is still cooperative inside a detached body, so call
Task.checkCancellation()at each checkpoint. Task { }is also unstructured, so the difference is inherited context, and neither one is canceled by a parent.
@MainActor
class ViewModel {
func load() {
Task {
heavyCPUWork() // Problem: still on the main actor because isolation is inherited
}
Task.detached(priority: .userInitiated) {
heavyCPUWork() // Fix: off the main thread, since nothing is inherited
await MainActor.run { self.updateUI() } // hop back explicitly
}
}
}
final class ThumbnailLoader {
private var task: Task<Void, Error>?
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
- When the inherited context is right but you want a different priority, use
Task(priority: .utility)instead of detaching. You keep the actor context and task-local values without the extra bookkeeping.



