← All topics/Concurrency

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

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.detached is a scalpel — use it when Task { } 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.
Task { } vs Task.detached — what each inherits
@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
        }
    }
}
Full pattern — store handle, cancel on teardown
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, use Task(priority: .utility) before reaching for detached — you keep structured lifetime without the manual overhead.