top of page

01 · INTRODUCTION

What Does Task Inheritance Mean in Swift?

The short answer

Task inheritance means that an unstructured task created with Task { } begins with important context from the place where it was created. That context can include actor isolation, task priority and task-local values.

CODE EXAMPLE

@MainActor
func refreshButtonTapped() {
    Task {
        // This task inherits MainActor isolation.
        isLoading = true
        launches = try await launchManager.fetchLaunches()
        isLoading = false
    }
}

The new task does not begin as an empty container. Because it was created inside a @MainActor-isolated function, its closure is also isolated to MainActor. The task may additionally inherit the priority and task-local values of the current task.

Important

Inheriting context does not make Task { } a structured child task. Actor isolation, priority and task-local values describe the task's starting context. They do not give it a child task's bounded lifetime or automatic cancellation relationship.

02 · TUTORIAL

Why a New Task Needs Context

A task is a schedulable unit of asynchronous work, but that work does not execute in a vacuum. The runtime needs to know where actor-isolated code is permitted to run. Scheduling can use a priority. Diagnostic or request-scoped information may need to remain available as the operation crosses asynchronous calls.

Imagine creating a task as handing work to a task manager. The manager does not receive only the closure. It also receives a small amount of context describing the environment from which the work was launched.

Conceptual model:

EXECUTION DIAGRAM

Creation context
  ├─ actor isolation ─┐
  ├─ task priority ───┼──▶ new Task { operation }
  └─ task-local values┘

Structured lifetime and cancellation are separate relationships.

This is a teaching model, not a diagram of the runtime's internal storage. Its purpose is to show that several kinds of context can cross the task-creation boundary while the creator and the new task still have independent lifetimes.

03 · TUTORIAL

What Task { } Inherits

When Task { } is called from an existing task, the new unstructured task inherits three forms of context:

• the current task's priority;

• the current task-local values;

• and the current actor's isolation and execution context when the closure is formed in actor-isolated code.

These values answer different questions.

Actor isolation: Which isolated state may this code access directly?

Priority: How urgent is this work relative to other eligible work?

Task-local values: Which scoped contextual values should be visible along this asynchronous operation?

The word inherit can sound as if the new task becomes a child of the current task. It does not. Here, inheritance describes context propagation, not a parent-child lifetime relationship.

Important Terminology

Task context is information carried with asynchronous work. Task structure is the lifetime relationship created by structured-concurrency tools such as async let and task groups. Context and structure are related ideas, but they are not interchangeable.

04 · TUTORIAL

Actor Isolation Is Inherited

Actor inheritance is usually the first form developers encounter because it directly affects which code compiles.

CODE EXAMPLE

@MainActor
final class LaunchFeature {
    private(set) var launches: [Launch] = []
    private(set) var isLoading = false

    private let launchManager: LaunchManager

    init(launchManager: LaunchManager) {
        self.launchManager = launchManager
    }

    func refresh() {
        Task {
            isLoading = true
            launches = try await launchManager.fetchLaunches()
            isLoading = false
        }
    }
}

refresh() is isolated to MainActor because the enclosing type is. The closure passed to Task { } inherits that isolation. It can therefore read and mutate isLoading and launches directly.

Without this rule, creating a task inside a main-actor feature would immediately discard the feature's isolation context. Every state access would need a separate actor transition, even though the task was launched specifically to perform feature-owned work.

Inheritance is established where the closure is formed

The same syntax can create tasks with different isolation because the surrounding context is different.

CODE EXAMPLE

@MainActor
func beginInterfaceRefresh() {
    Task {
        // MainActor-isolated context is inherited here.
        updateLoadingIndicator()
    }
}

func beginIndependentWork() {
    Task {
        // No actor isolation is inherited from this function.
        await refreshCache()
    }
}

The braces alone do not say where the operation runs. To understand a task closure, read the declaration containing it. A surrounding global actor, actor method or isolated parameter can be part of the answer.

Actor inheritance is not background execution

Creating a task from MainActor does not move its synchronous work to a background thread.

CODE EXAMPLE

@MainActor
func calculateVisibleLaunches() {
    Task {
        // Still MainActor-isolated synchronous work.
        visibleLaunches = allLaunches.filter(expensiveVisibilityCheck)
    }
}

The filter contains no suspension point and executes as synchronous main-actor work. If it takes a long time, it can delay other work that needs MainActor, including interface updates and event handling.

Actor isolation is a correctness rule. It protects isolated state by controlling access. It is not a promise that work is off the main thread, and Task { } is not a dispatch-to-background operation.

05 · TUTORIAL

Priority Is Inherited

When code creates Task { } from an existing task and does not provide an explicit priority, the new task begins with the current task's priority.

CODE EXAMPLE

func refreshLaunches() async {
    print("Creator:", Task.currentPriority)

    let refreshTask = Task {
        print("New task:", Task.currentPriority)
        await launchManager.refreshCache()
    }

    await refreshTask.value
}

The two reported priorities begin from the same value. This allows related work to retain the urgency of the operation that launched it instead of silently returning to one universal default.

You can request a priority explicitly:

CODE EXAMPLE

let refreshTask = Task(priority: .utility) {
    await launchManager.refreshCache()
}

That should be a deliberate scheduling decision, not routine decoration on every task. Inheritance usually preserves useful intent with less configuration.

Important

Task priority is scheduling information. It does not select a thread, guarantee a start time or force tasks to complete in priority order. The runtime can also escalate priority in some circumstances, such as when higher-priority work waits for a task's value.

06 · TUTORIAL

Task-Local Values Are Inherited

A task-local value is scoped contextual information associated with a task. It is useful for values such as request identifiers, trace identifiers and diagnostic metadata that should follow an asynchronous operation without being added to every function parameter.

CODE EXAMPLE

enum LaunchContext {
    @TaskLocal static var refreshID: UUID?
}

A value is bound for the dynamic duration of an operation with withValue:

CODE EXAMPLE

func refreshLaunches() async {
    let refreshID = UUID()

    await LaunchContext.$refreshID.withValue(refreshID) {
        await recordRefreshStarted()
        await fetchAndStoreLaunches()
    }
}

Functions called inside the operation can read LaunchContext.refreshID. When a new task is created with Task { } inside the binding, it inherits the bound value.

CODE EXAMPLE

await LaunchContext.$refreshID.withValue(UUID()) {
    let task = Task {
        print(LaunchContext.refreshID as Any)
        await launchManager.refreshCache()
    }

    await task.value
}

The new task receives its own inherited task-local context. The binding remains scoped: after the withValue operation finishes, the surrounding code sees the previous value again.

Task-local values are context, not shared mutable storage

A task-local is read through its wrapped property. Code introduces a value using the projected property's scoped withValue operation rather than assigning to the wrapped property.

This makes task locals a good match for observational context. It does not make them a replacement for ordinary parameters, actor-isolated model state or a database. A launch identifier needed to produce a correct business result should normally remain an explicit input. A trace identifier used only to correlate logs can be an appropriate task-local value.

07 · TUTORIAL

What Inheritance Does Not Include

Task { } creates an unstructured task. Its inherited context does not give it the lifetime rules of a structured child.

CODE EXAMPLE

func startRefresh() async {
    Task {
        await launchManager.refreshCache()
    }

    // startRefresh() does not automatically await that task.
}

The new task can continue after startRefresh() returns. If the task running startRefresh() is cancelled, that cancellation is not automatically propagated into the unstructured task merely because it was created there.

Conceptual comparison:

CODE EXAMPLE

Task { }
  inherits: actor context, priority, task-local values
  does not inherit: structured lifetime or automatic creator cancellation

Structured child
  belongs to: the surrounding async-let or task-group scope
  cannot outlive: that structured scope

Calling the first task a child because it inherited values would hide the most important architectural difference. The creator may retain its handle, await its value and explicitly cancel it, but those are ownership decisions made by your code.

Cancellation status is not one of the inherited values

Suppose a cancelled task creates an unstructured task:

CODE EXAMPLE

func beginReplacementRefresh() async {
    guard Task.isCancelled else { return }

    let replacement = Task {
        print(Task.isCancelled)
    }

    await replacement.value
}

The new unstructured task is not automatically born cancelled. If the architecture requires cancellation to follow from the creating operation, make that relationship explicit—usually by retaining the handle and calling cancel(), or by keeping the work inside structured concurrency.

08 · TUTORIAL

A Complete Launch-Feature Example

The following feature uses all three inherited values for distinct purposes. It is isolated to MainActor, refresh work inherits the caller's priority, and a task-local refresh identifier follows the operation into diagnostic code.

CODE EXAMPLE

enum LaunchContext {
    @TaskLocal static var refreshID: UUID?
}

actor LaunchStore {
    func fetchUpcomingLaunches() async throws -> [Launch] {
        log("Fetching launches")
        return try await launchAPIRequest()
    }

    private func log(_ message: String) {
        print(
            message,
            "refresh:", LaunchContext.refreshID as Any,
            "priority:", Task.currentPriority
        )
    }
}

@MainActor
final class LaunchFeature {
    private(set) var launches: [Launch] = []
    private(set) var isLoading = false
    private(set) var errorMessage: String?

    private let store: LaunchStore
    private var refreshTask: Task<Void, Never>?

    init(store: LaunchStore) {
        self.store = store
    }

    func refresh() {
        refreshTask?.cancel()

        let refreshID = UUID()

        refreshTask = Task {
            await LaunchContext.$refreshID.withValue(refreshID) {
                isLoading = true
                defer { isLoading = false }

                do {
                    let loaded = try await store.fetchUpcomingLaunches()
                    try Task.checkCancellation()
                    launches = loaded
                    errorMessage = nil
                } catch is CancellationError {
                    // A newer refresh now owns the feature's result.
                } catch {
                    errorMessage = "Could not load launches."
                }
            }
        }
    }

    func stop() {
        refreshTask?.cancel()
        refreshTask = nil
        isLoading = false
    }
}

The task closure inherits MainActor isolation from refresh(), so it can update feature state. When it calls the actor-isolated store, the task can suspend while that actor performs its work. When main-actor-isolated code becomes eligible again, execution returns through MainActor.

The task also inherits the current task's priority when one is available. The example does not override it because the refresh should normally carry the urgency of the user-facing operation that initiated it.

The refresh identifier is introduced inside the new task and is visible to calls made within that scoped binding. It is diagnostic context, not feature state.

Finally, the feature retains the handle because context inheritance does not provide lifecycle ownership. Calling stop() explicitly requests cancellation. Beginning another refresh cancels the previous handle before replacing it.

09 · TUTORIAL

How to Read Task Creation in a Codebase

When you encounter Task { }, read outward before reading inward.

1. Identify whether the surrounding declaration is actor-isolated.

2. Identify the current asynchronous operation whose priority may be inherited.

3. Look for active task-local bindings in the call chain.

4. Then ask who owns the returned task handle.

The first three questions explain the task's starting context. The fourth explains its lifetime. Keeping those groups separate makes concurrency code easier to describe during review:

“This refresh task inherits the launch feature's main-actor isolation and the caller's priority. The feature owns its handle and cancels it when the refresh is replaced.”

That sentence communicates more than saying that the code “runs something asynchronously.” It identifies the permissions, scheduling intent and ownership policy that shape the operation.

10 · TUTORIAL

Task { } and Task.detached { }

Task.detached { } creates an unstructured task that is independent of the current context. It does not inherit actor isolation, priority or task-local values in the way Task { } does.

CODE EXAMPLE

Task {
    // Inherits available creation context.
}

Task.detached {
    // Starts independently of that context.
}

That independence is a semantic choice, not an optimisation and not a general solution for isolation errors. If code should update actor-owned feature state, erasing the actor relationship is usually the opposite of what the design needs.

The detailed rules, valid use cases and risks of detached work belong to the next article. For this article, the useful contrast is simple: Task { } preserves available context; Task.detached { } deliberately does not.


11 · TUTORIAL

The Complete Mental Model

Conceptual model:

EXECUTION DIAGRAM

Current execution
  ├─ actor isolation
  ├─ priority
  └─ task-local values
             │
             │ Task { operation }
             ▼
New unstructured task
  ├─ begins with inherited context
  ├─ has its own lifetime
  ├─ is not automatically awaited by its creator
  └─ is not automatically cancelled with its creator

Task inheritance answers, “What context does this new task begin with?” Structured concurrency answers, “Which scope owns this child work and bounds its lifetime?” An application needs both kinds of reasoning, but one does not imply the other.


12 · TUTORIAL

What to Remember

Task { } inherits available context from its creation point.

• That context includes actor isolation, priority and task-local values.

• Actor inheritance allows a task closure to access the surrounding actor's isolated state.

• Inheriting MainActor does not create background execution.

• Priority is scheduling information, not a thread choice or completion-order guarantee.

• Task-local values are scoped context, especially useful for diagnostics and tracing.

• Context inheritance does not make an unstructured task a structured child.

• The creator does not automatically await the new task.

• Creator cancellation does not automatically propagate into the new unstructured task.

• Ownership of the task handle must remain an explicit architectural decision.

Task.detached { } deliberately starts without inheriting the same creation context.


13 · TUTORIAL

Frequently Asked Questions

Does Task { } inherit MainActor?

It inherits MainActor when its closure is formed in a MainActor-isolated context. The syntax does not always mean MainActor; the surrounding isolation determines what is available to inherit.

Does an inherited actor context mean the task stays on one thread?

No. Actor isolation is enforced through executors and does not promise permanent attachment to one system thread. A task can suspend, execute asynchronous callees with their own isolation requirements and later resume actor-isolated work through the appropriate executor.

Does Task { } inherit cancellation?

No. An unstructured task does not automatically inherit the creator's cancellation status, and later cancellation of the creator does not automatically cancel it. Use structured concurrency or explicitly connect cancellation through the task handle when the work should share that lifecycle.

Does Task { } inherit priority?

Yes, when created from an existing task without an explicit priority, it begins with the current task's priority. Priority remains a scheduling signal and can be escalated in some circumstances.

Are task-local values copied or shared?

The new unstructured task receives inherited task-local bindings. Treat them as scoped, read-only context rather than shared mutable state. New bindings made inside one task do not mutate another task's binding.

Is Task { } a child task if I immediately await its value?

No. Awaiting the handle is a useful ownership choice, but it does not change how the task was created. It remains an unstructured task rather than a child produced by async let or a task group.

Should I use Task.detached to avoid inheriting MainActor?

Not as a general escape hatch. First decide which component and isolation domain should own the work. Detached tasks intentionally discard context and introduce stricter data-transfer requirements; that choice should match the architecture.


15 · TUTORIAL

Continue Learning

Task { } preserves available creation context. Sometimes an operation must intentionally begin without that relationship. The next article, What Is Task.detached in Swift?, will explain what detached work gives up, when that independence is appropriate and why detachment is not simply a way to request a background thread.


16 · TUTORIAL

Download Xcode Playground

Use the accompanying Understanding Task Inheritance.playground to observe inherited priority and task-local values, compare actor-isolated and nonisolated creation sites, and demonstrate that context inheritance does not propagate cancellation or create a structured child-task lifetime.

bottom of page