01 · INTRODUCTION
What Is Task.detached in Swift?
The short answer
Task.detached { } creates a new unstructured task that begins independently of the context from which it was launched. It does not inherit the current actor isolation, task priority or task-local values.
CODE EXAMPLE
let indexTask = Task.detached(priority: .utility) {
buildSearchIndex(from: launches)
}
The returned value is a task handle. The detached task starts running independently, and other asynchronous code can await its result or request its cancellation through that handle.
Important
Task.detached means detached from the current task context. It does not mean “create a new thread,” “make this code thread-safe” or “run this code faster.”
02 · TUTORIAL
Why Detached Tasks Exist
The ordinary Task { } initializer preserves the context in which its closure is formed. This is usually exactly what an application needs.
CODE EXAMPLE
@MainActor
func refreshButtonTapped() {
Task {
isLoading = true
launches = try await launchManager.fetchLaunches()
isLoading = false
}
}
The task inherits MainActor isolation. It can update the interface-owned state directly. It can also inherit the current task's priority and task-local values.
A detached task is for work that should intentionally begin without that relationship.
CODE EXAMPLE
@MainActor
func rebuildSearchIndex() async throws {
let snapshot = launches
let index = try await Task.detached(priority: .utility) {
try SearchIndex.build(from: snapshot)
}.value
searchIndex = index
}
The detached closure does not inherit MainActor. It receives a snapshot of the launch data, performs independent work and returns a value. The surrounding main-actor function awaits that value and then publishes it into main-actor-isolated state.
This is the essential detached-task shape:
EXECUTION DIAGRAM
isolated owner ── sends value ──▶ detached operation
isolated owner ◀─ awaits value ── detached operation
The diagram is conceptual. It shows the transfer of values and responsibility, not a promise about particular system threads.
03 · TUTORIAL
Task { } and Task.detached { } Are Different
Both APIs create unstructured tasks. Both return a handle. Both tasks can outlive the function that creates them. The difference is their relationship to the creation context.
CODE EXAMPLE
Task {
// Inherits available context.
}
Task.detached {
// Begins independently of that context.
}
Property
Actor isolation
Task { }
Inherits available actor context
Task.detached { }
Does not inherit actor context
Property
Priority
Task { }
Inherits current task priority by default
Task.detached { }
Does not inherit current task priority
Property
Task-local values
Task { }
Inherited
Task.detached { }
Not inherited
Property
Structured child
Task { }
No
Task.detached { }
No
Property
Automatic creator cancellation
Task { }
No
Task.detached { }
No
Property
Returns a handle
Task { }
Yes
Task.detached { }
Yes
The last three rows matter. An ordinary unstructured task already has an independent lifetime. Detachment removes inherited context; it does not introduce unstructured lifetime for the first time.
Important Terminology
A detached task is an unstructured top-level task that does not inherit the actor isolation, priority or task-local values of the context that creates it.
04 · TUTORIAL
A Detached Task Does Not Inherit Actor Isolation
Actor isolation controls which code may access actor-owned state. An ordinary task formed inside an actor inherits that actor's isolation. A detached task does not.
CODE EXAMPLE
@MainActor
final class LaunchFeature {
private(set) var launches: [Launch] = []
func demonstrateDifference() {
Task {
launches.removeAll() // Allowed: MainActor inherited.
}
Task.detached {
// launches.removeAll()
// Error: MainActor-isolated state cannot be mutated here.
}
}
}
The compiler error is useful. The detached operation is independent, so it cannot reach back into isolated feature state as though it still belonged to the feature's executor.
The solution is not to suppress the isolation rule. Give the detached operation the values it needs and return the value it produces.
CODE EXAMPLE
struct Launch: Sendable {
let id: UUID
let missionName: String
}
struct SearchIndex: Sendable {
let missionsByID: [UUID: String]
nonisolated static func build(from launches: [Launch]) -> SearchIndex {
SearchIndex(
missionsByID: Dictionary(
uniqueKeysWithValues: launches.map {
($0.id, $0.missionName.lowercased())
}
)
)
}
}
These value types conform to Sendable. A snapshot can cross into the detached task, and the completed index can cross back to the awaiting caller.
Detachment is an isolation decision
Developers sometimes use Task.detached merely because a compiler error says that actor-isolated state cannot be accessed. That reverses the reasoning process.
First decide where the work belongs. If it belongs to the feature and needs direct access to feature state, preserve the feature's isolation. If it is genuinely independent work, design a clear Sendable input-and-output boundary.
Detachment should describe architecture. It should not hide an architecture that has not been decided.
05 · TUTORIAL
A Detached Task Does Not Mean a Background Thread
Task.detached creates work that is not actor-isolated by inheritance. Swift schedules its eligible jobs using the concurrency runtime and executors. The runtime ultimately uses system threads, but the API does not allocate one dedicated background thread to the closure.
Conceptual execution model:
EXECUTION DIAGRAM
Task.detached { operation }
│
▼
detached task containing schedulable jobs
│
▼
Swift concurrency executor
│
▼
available system thread while each job executes
A task can suspend and later resume. Different portions of its lifetime are not required to execute on one permanent thread.
For developers moving from Grand Central Dispatch, Task.detached can initially feel similar to submitting independent work to a global dispatch queue:
CODE EXAMPLE
DispatchQueue.global(qos: .utility).async {
rebuildIndex()
}
Task.detached(priority: .utility) {
rebuildIndex()
}
This is a useful historical bridge, but the models are not identical. A detached task participates in Swift's task system. It has a typed result, can suspend at asynchronous calls, carries cancellation state and is scheduled cooperatively by the Swift Concurrency runtime.
Important
Task.detached is a modern way to schedule independent asynchronous work. It is not a safety mechanism. Actor isolation, Sendable checking and deliberate ownership provide the safety.
06 · TUTORIAL
Priority and Task-Local Values Start Independently
A detached task does not inherit the current task's priority. If its urgency matters, state that decision at creation.
CODE EXAMPLE
let task = Task.detached(priority: .utility) {
print(Task.currentPriority)
return buildSearchIndex()
}
Priority remains a scheduling signal. It does not choose a thread or guarantee when the work begins or finishes.
Detached tasks also do not inherit task-local values.
CODE EXAMPLE
enum LaunchContext {
@TaskLocal static var refreshID: String?
}
await LaunchContext.$refreshID.withValue("refresh-42") {
await Task {
print(LaunchContext.refreshID as Any)
// Optional("refresh-42")
}.value
await Task.detached {
print(LaunchContext.refreshID as Any)
// nil
}.value
}
That absence is part of the meaning of detachment. A diagnostic identifier, tracing context or task-scoped dependency does not silently cross the boundary.
If the independent operation still needs a particular value, pass it as an explicit input:
CODE EXAMPLE
let refreshID = LaunchContext.refreshID
let task = Task.detached {
await logger.recordIndexBuild(refreshID: refreshID)
}
Explicit transfer makes the dependency visible. Do not re-create every task-local binding inside a detached task by habit; doing so may be a sign that the operation was not meant to be detached.
07 · TUTORIAL
A Detached Task Is Still Unstructured
The function that creates a detached task does not automatically wait for it.
CODE EXAMPLE
func beginIndexBuild() {
Task.detached {
await rebuildEverySearchIndex()
}
// The function returns while the task may still be running.
}
Discarding the handle does not cancel the task. It removes the simplest way to await its result, observe its error or request cancellation.
A detached task is therefore a strong ownership boundary. Ask:
• Who retains the handle?
• Who needs the result?
• Who observes failure?
• Who requests cancellation?
• May the work legitimately continue when the initiating feature disappears?
If those questions have no clear answer, the work probably belongs in an ordinary async function, an inherited Task { }, an async let binding or a task group.
08 · TUTORIAL
Cancellation Must Be Connected Explicitly
A detached task does not become a structured child of the task that creates it. Cancellation of the surrounding task does not automatically cancel the detached operation.
CODE EXAMPLE
func buildIndex() async throws -> SearchIndex {
let detached = Task.detached {
try SearchIndex.buildCheckingCancellation(from: launches)
}
return try await detached.value
}
Awaiting detached.value does not transform the detached task into a child. If the task running buildIndex() is cancelled, the detached task still needs an explicit cancellation request.
A cancellation handler can connect those lifecycles:
CODE EXAMPLE
func buildIndex() async throws -> SearchIndex {
let snapshot = launches
let detached = Task.detached(priority: .utility) {
try SearchIndex.buildCheckingCancellation(from: snapshot)
}
return try await withTaskCancellationHandler {
try await detached.value
} onCancel: {
detached.cancel()
}
}
The handler forwards cancellation to the detached task's handle. The detached computation must then cooperate by checking its cancellation state.
CODE EXAMPLE
extension SearchIndex {
nonisolated static func buildCheckingCancellation(
from launches: [Launch]
) throws -> SearchIndex {
var values: [UUID: String] = [:]
for launch in launches {
try Task.checkCancellation()
values[launch.id] = launch.missionName.lowercased()
}
return SearchIndex(missionsByID: values)
}
}
Calling cancel() marks the task as cancelled. It does not forcibly terminate synchronous code. The loop checks cooperatively and throws CancellationError.
09 · TUTORIAL
When Task.detached Is Appropriate
A detached task is appropriate when independence is the requirement rather than an accidental side effect.
The work should not inherit actor isolation
A self-contained computation can receive immutable, Sendable values and produce a new Sendable result. It does not need direct access to actor-owned state.
The work should not inherit contextual values
A maintenance operation may intentionally start with its own priority and without request-scoped task-local context.
The lifetime is independently owned
A service may retain the task handle, define its cancellation policy and observe its completion independently of the short-lived method that launched it.
These conditions should appear together as a coherent design. The mere desire to “get off the main actor” is not enough. Often the better design is an async function with explicit isolation, an actor that owns the work, or structured concurrency at the call site.
10 · TUTORIAL
Common Misuses of Task.detached
Using it to make unsafe state safe
CODE EXAMPLE
final class LaunchCache {
var launches: [Launch] = []
}
let cache = LaunchCache()
Task.detached {
cache.launches.append(newLaunch)
}
Detachment does not protect the mutable array. Under strict concurrency checking, transferring a non-Sendable reference into independently executing code is exactly the kind of design the compiler should question.
Give mutable shared state an isolation domain, such as an actor:
CODE EXAMPLE
actor LaunchCache {
private var launches: [Launch] = []
func insert(_ launch: Launch) {
launches.append(launch)
}
}
Task.detached {
await cache.insert(newLaunch)
}
The actor provides the safety. The detached task merely calls it across the actor boundary.
Using it for ordinary network requests
CODE EXAMPLE
Task.detached {
try await URLSession.shared.data(from: url)
}
An asynchronous network request already suspends while waiting. It does not need detachment merely to avoid blocking the caller. Preserve structure and context unless the operation genuinely needs an independent lifetime.
Using it to create parallelism everywhere
When several concurrent operations belong to one result, structured concurrency usually expresses that relationship more accurately.
CODE EXAMPLE
async let launches = launchAPI.fetchLaunches()
async let rockets = rocketAPI.fetchRockets()
return try await LaunchPage(
launches: launches,
rockets: rockets
)
The child tasks remain bounded by the current operation. Errors, waiting and cancellation have a visible structure. Detached tasks would discard those relationships without improving the model.
Discarding the handle without deciding ownership
Fire-and-forget work can lose errors and outlive the feature that initiated it. If completion matters, retain the handle or expose an async function that callers can await.
11 · TUTORIAL
A Complete Launch-Feature Example
The launch feature below builds a search index from an immutable snapshot. The detached task owns only the computation. The main-actor feature owns interface state and the operation's lifecycle.
CODE EXAMPLE
struct Launch: Sendable {
let id: UUID
let missionName: String
}
struct SearchIndex: Sendable {
let missionsByID: [UUID: String]
nonisolated static func build(from launches: [Launch]) throws -> SearchIndex {
var result: [UUID: String] = [:]
for launch in launches {
try Task.checkCancellation()
result[launch.id] = launch.missionName.lowercased()
}
return SearchIndex(missionsByID: result)
}
}
@MainActor
final class LaunchFeature {
private(set) var launches: [Launch] = []
private(set) var searchIndex: SearchIndex?
private(set) var isBuildingIndex = false
private(set) var errorMessage: String?
private var indexTask: Task<Void, Never>?
func replaceLaunches(with newValue: [Launch]) {
launches = newValue
rebuildSearchIndex()
}
func rebuildSearchIndex() {
indexTask?.cancel()
let snapshot = launches
isBuildingIndex = true
indexTask = Task {
let detached = Task.detached(priority: .utility) {
try SearchIndex.build(from: snapshot)
}
do {
let completed = try await withTaskCancellationHandler {
try await detached.value
} onCancel: {
detached.cancel()
}
try Task.checkCancellation()
searchIndex = completed
errorMessage = nil
} catch is CancellationError {
// A newer snapshot now owns the result.
} catch {
errorMessage = "Could not build the launch index."
}
if !Task.isCancelled {
isBuildingIndex = false
indexTask = nil
}
}
}
func stop() {
indexTask?.cancel()
indexTask = nil
isBuildingIndex = false
}
}
The outer Task { } inherits MainActor, so it can coordinate feature state. The inner detached task receives a Sendable snapshot and returns a Sendable index without directly touching the feature.
The feature retains the outer handle. Its cancellation handler explicitly forwards cancellation to the detached handle. The index builder checks cancellation during synchronous work. The completed value is published only after the outer task confirms that it has not been cancelled.
This amount of code is revealing. Detachment removes relationships that ordinary tasks and structured children would otherwise preserve. When independence is correct, that explicitness is valuable. When independence is unnecessary, the same ceremony is a warning that a simpler concurrency structure would be better.
12 · TUTORIAL
The Complete Mental Model
Conceptual model:
EXECUTION DIAGRAM
Current task or actor
├─ actor isolation ───────╳
├─ current priority ──────╳── not inherited
├─ task-local values ─────╳
└─ cancellation ──────────╳
│
Task.detached { operation }
│
▼
independent top-level task
├─ typed result
├─ task handle
├─ cooperative cancellation
└─ explicit ownership required
Detached does not mean outside Swift Concurrency. It means outside the current task's inherited context and structured lifetime. The runtime still schedules the task. The compiler still checks isolation and Sendable boundaries. The task still suspends, resumes, returns a value and responds cooperatively to cancellation.
13 · TUTORIAL
What to Remember
• Task.detached { } creates an unstructured top-level task.
• It does not inherit actor isolation, task priority or task-local values.
• It returns a typed task handle that can be awaited or cancelled.
• It is not a structured child of the task that creates it.
• Cancellation does not automatically propagate from the creator.
• Discarding the handle does not cancel the detached task.
• Detachment does not create a dedicated thread.
• Detachment does not make shared mutable state safe.
• Actor isolation and Sendable checking provide safety across concurrency boundaries.
• Pass independent Sendable values into detached work and return a Sendable result.
• Prefer structured concurrency when several operations belong to one parent result.
• Use detachment only when losing the creation context is part of the intended architecture.
14 · TUTORIAL
Frequently Asked Questions
Does Task.detached run on a background thread?
Not as a dedicated-thread guarantee. The detached task is not actor-isolated by inheritance and its eligible jobs are scheduled through Swift's concurrency executors onto available system threads.
Is Task.detached parallel?
It can execute concurrently with other work and may execute in parallel when runtime resources are available. Parallel execution is not guaranteed merely because the task is detached.
Is Task.detached safer than DispatchQueue.global?
Detachment itself is not a safety feature. Swift's isolation rules, Sendable checking and typed task model can make concurrency relationships more visible, but the developer must still design ownership and protect mutable state.
Does Task.detached inherit MainActor?
No. A detached task does not inherit the surrounding actor context. Accessing MainActor-isolated state from it requires an explicit actor transition, but independent computation should usually exchange values rather than repeatedly reaching into interface state.
Does Task.detached inherit cancellation?
No. Retain the handle and explicitly call cancel() when cancellation should cross into the detached operation. The detached operation must cooperate by checking cancellation or calling cancellation-aware APIs.
Should I use Task.detached for a URLSession request?
Usually not. An awaited URLSession request already suspends while waiting. Detach it only when the entire operation intentionally requires independent context and lifetime—not merely because networking should not block the interface.
Should I use Task.detached for CPU-intensive work?
Detachment can express a self-contained computation that should not inherit actor isolation, but it is not the only design. The computation should accept Sendable inputs, return a Sendable value, cooperate with cancellation and have a clear owner.
When should I prefer Task { }?
Prefer Task { } when the operation should preserve the surrounding actor context, priority or task-local values. Prefer an ordinary async call or structured child when the work belongs to the current asynchronous operation.
15 · TUTORIAL
References
16 · TUTORIAL
Continue Learning
A detached task creates a particularly strong boundary: data must cross into independently executing code without relying on the creator's actor isolation. The next article, What Does Sendable Mean at a Task Boundary?, will explain how Swift decides which values may safely cross that boundary and why value transfer is central to modern concurrent architecture.
17 · TUTORIAL
Download Xcode Playground
Use the accompanying Understanding Task.detached.playground to compare inherited and detached tasks, observe priority and task-local behaviour, encounter an actor-isolation compiler error, pass a Sendable launch snapshot into independent work and explicitly forward cancellation through a retained task handle.
