top of page

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.


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.

bottom of page