Task {} vs Task.detached

Concurrency Mid-level Swift 6.0+ iOS 17+ Reviewed By Alok Singh

Pick the fix

In a @MainActor view model, Task.detached { self.items = try await api.load() } fails to compile in Swift 6. Best fix?

Options
Hint 1 of 2

What does Task {} inherit from a @MainActor context?

Hint 2 of 2

Does an await on the main actor block the UI?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Task { items = try await api.load() }

Correct. The task inherits the main actor, so items can be set directly while load() suspends without blocking.

Why not the others?
  • B Task.detached(priority: .high) { ... }. Priority doesn't change isolation; it still can't set main-actor state synchronously.
  • C Mark items as nonisolated. A mutable stored property of a main-actor class can't be nonisolated; it would be a data race.
  • D Wrap the assignment in DispatchQueue.main.async inside the detached task. This mixes GCD into async code and loses structured error handling. It only hides the real fix.

30-second answer

Task {} inherits the current actor, priority and task-local values, so inside a @MainActor view model it runs on the main actor. Task.detached inherits none of them. Detached tasks are rarely needed: to run heavy work off the main actor, call a @concurrent or nonisolated async function from a normal Task instead.

Practise saying it · 30 s. No recording, just a timer

Compare Task {} and Task.detached.

Say your answer out loud, then check which of these points you covered:

  • Task {} inherits actor, priority, task-locals
  • Detached inherits none of them
  • Prefer @concurrent functions for off-main work

Deep dive

Both create unstructured tasks: keep a handle if you need to cancel them.

A detached closure can't touch main-actor state without await, and anything it captures must be Sendable.

In Swift 6.2, @concurrent is the clear way to say 'this function runs on the concurrent pool'.

Prefer structured tasks (async let, groups, .task) over either kind where you can.

Heavy work off the main actor (Swift 6.2) Type-checked · Swift 6.4
@MainActor @Observable
final class PhotoModel {
    var thumbnail: UIImage?
    func load(_ data: Data) {
        Task { thumbnail = await Self.decode(data) }
    }
    @concurrent
    static func decode(_ data: Data) async -> UIImage? {
        UIImage(data: data)?.preparingThumbnail(of: CGSize(width: 200, height: 200))
    }
}

Common mistakes

  • Using Task.detached to 'get off the main thread' and then capturing UIKit views in it.
  • Assuming Task {} always runs in the background.
  • Never storing or cancelling unstructured tasks.

Senior take & follow-ups

Seniors say 'detached is a code smell unless you can explain which inheritance you are avoiding'.

Follow-up questions

Think of your answer first, then open the hint.

  1. Does Task {} inherit cancellation from the parent?

    Show hint

    No. Only structured children are cancelled with the parent.

  2. What's a legitimate use of Task.detached?

    Show hint

    Low-priority work that must not inherit task-locals or the current actor, such as cache cleanup.

Suggest a fix if something here is wrong or out of date.