Chapter 3 — Tasks: launching and awaiting work
What a task is
- To bridge synchronous code into async — the button handler,
viewDidLoad, an app-launch hook — - To run work concurrently — though for that, structured concurrency (Part II) is almost always
// The basic shape: a Task launched from synchronous code.
Task {
let profile = try await client.fetch(Profile.self, from: profileURL)
print(profile.name)
}
// This line runs IMMEDIATELY — the Task's body runs concurrently, not now.
print("launched") // may print before or after the fetch completes
Tasks inherit their context
flowchart TB
CTX["Context that creates the Task
(e.g. a @MainActor view)"] CTX -->|"inherits"| A["Actor isolation
(runs on the same actor)"] CTX -->|"inherits"| P["Priority
(same priority as the creator)"] CTX -->|"inherits"| T["Task-local values
(propagated down)"] A --> TASK["Task { }"] P --> TASK T --> TASK
(e.g. a @MainActor view)"] CTX -->|"inherits"| A["Actor isolation
(runs on the same actor)"] CTX -->|"inherits"| P["Priority
(same priority as the creator)"] CTX -->|"inherits"| T["Task-local values
(propagated down)"] A --> TASK["Task { }"] P --> TASK T --> TASK
// Inside a SwiftUI View (which is @MainActor):
Button("Load") {
Task {
let profile = try? await client.fetch(Profile.self, from: profileURL)
self.profile = profile // ✅ safe — this Task inherited @MainActor, so we're on main
}
}
Task.detached: inherits nothing
// Inside the same @MainActor view:
Task.detached {
let profile = try? await client.fetch(Profile.self, from: profileURL)
self.profile = profile // ❌ ERROR in Swift 6 — this is NOT on the main actor
}
Getting a task's result
// A Task<Success, Failure> is a handle you can await.
let task = Task {
try await client.fetch(Profile.self, from: profileURL)
}
// … do other things while it runs …
let profile = try await task.value // await the result (rethrows the task's error)
task.value—awaits and returns the success value, rethrowing if the task threw.task.result—awaits and returns aResult<Success, Failure>you canswitchon withouttry.
Priorities
Task(priority: .background) {
await prefetchNextPage()
}
The catch: Task { } is unstructured
func onAppear() {
Task {
await longRunningWork() // this keeps running even after onAppear() returns
}
// onAppear returns HERE, but the task lives on. Who cancels it? You must.
}
- Hold the handle if you might need to cancel it. Store the
Task(e.g., in a property) and call - Prefer structured concurrency for concurrent work. When work has a clear scope — "do these
flowchart TB
subgraph UN__Unstructured__Task_______ [\"Unstructured (Task { })\"]
U1["you launch it"] --> U2["you must cancel it"]
U2 --> U3["you must manage its lifetime"]
end
subgraph ST__Structured__async_let___groups___ [\"Structured (async let / groups)\"]
S1["scope launches children"] --> S2["scope auto-waits"]
S2 --> S3["scope auto-cancels on exit/error"]
end
What we built in this chapter
- Established the task as the fundamental unit of async work — every
awaitruns inside one — and - Learned that
Task { }inherits actor isolation, priority, and task-local values from its - Contrasted
Task.detached { }, which inherits nothing, and established that it's a deliberate - Saw a task as a handle (
task.value/task.result) for start-now-collect-later, and used - Confronted the key hazard:
Task { }is unstructured — its lifetime isn't bound to any scope — so
Mental model to take away
- A task is a cheap, runtime-scheduled unit of async work; the async equivalent of a thread, but
Task { }inherits context (crucially, actor isolation) — that inheritance is what makes UI- Unstructured tasks are your responsibility: nothing waits for or cancels them automatically. Use
- Let priority be inherited unless you specifically know a piece of work is more or less urgent than