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.
14 · TUTORIAL
References
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.
