Yeda AI Tips · #135

Español

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:

PropertyPlain TaskTask.detached
Task priorityInherited from the current taskRuns at default; must be set explicitly
Task-local valuesInheritedLost
Actor isolationInherited from the enclosing contextNone — 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:

  1. Is priority set on purpose? If not, it's running at the default, not the caller's — often a subtle slowdown.
  2. Who cancels it? If nothing holds the handle, the work can outlive its reason to exist. Structured concurrency (async let, task groups) or a plain Task tied to a lifecycle is usually better.
  3. 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

Shipping Swift with an AI assistant? Yeda AI audits AI-generated code for the patterns that pass review but bite in production.

Talk to us · Read the blog