01 · INTRODUCTION
What Is a Task in Swift?
The short answer
A Swift Task is a unit of asynchronous work managed by the Swift Concurrency runtime.
It represents the complete lifetime of one asynchronous operation. A task can begin executing, reach a suspension point, stop occupying its current thread and later continue when its awaited work becomes ready.
A Task is not a Thread.
CODE EXAMPLE
Task {
let launches = try await loadRocketLaunches()
print(launches)
}
This code creates a task. It does not create a new thread.
💡 Important Terminology
Task = one logical asynchronous operation managed by Swift.
Task handle = the value returned by Task {}, which lets other code await the result or request cancellation.
Thread = an operating-system execution context on which task instructions may run.
02 · TUTORIAL
From the Task Manager to Task {}
In Swift Concurrency Runtime, we imagined that the application created a task manager during startup.
Our conceptual code looked something like this:
CODE EXAMPLE
let taskManager = TaskManager()
taskManager.submit {
try await loadRocketLaunches()
}
TaskManager was not a real Swift type. It represented the runtime machinery that accepts asynchronous work and arranges for eligible parts of that work to execute.
The real Swift expression is much smaller:
CODE EXAMPLE
Task {
try await loadRocketLaunches()
}
The closure describes the operation.
Task {} creates its task and submits its initial work to Swift's concurrency system. The task can begin running as soon as the runtime and its executor allow it to make progress.
EXECUTION DIAGRAM
CONCEPTUAL MODEL
Task {
try await loadRocketLaunches()
}
│
▼
Create asynchronous task
│
▼
Initial work becomes eligible
│
▼
Executor arranges execution
│
▼
System thread runs instructions
We create the task. Swift manages its execution.
03 · TUTORIAL
A Task Is the Lifetime of an Operation
The closure passed to Task gives the operation a beginning and an end.
CODE EXAMPLE
Task {
print("Refresh started")
let launches = try await loadRocketLaunches()
print("Loaded \(launches.count) launches")
}
The task begins before the first print and completes after the final print.
That lifetime can include time during which none of the task's instructions are executing.
EXECUTION DIAGRAM
ONE TASK
Created
│
▼
Running: print("Refresh started")
│
▼
Running: begin loadRocketLaunches()
│
▼
Suspended: waiting for network result
│
▼
Runnable: network result is available
│
▼
Running: print the launch count
│
▼
Completed
The waiting period still belongs to the task's lifetime.
This is why a task is more than a closure placed onto a queue. It is a managed asynchronous operation that can change state as it progresses.
04 · TUTORIAL
Every Async Function Executes Inside a Task
An async function does not create its own task every time it is called.
CODE EXAMPLE
func loadRocketLaunches() async throws -> [Launch] {
let data = try await downloadLaunchData()
return try decodeLaunches(from: data)
}
This function defines work that may suspend.
When an existing task calls it, that same task begins executing the function:
CODE EXAMPLE
Task {
let launches = try await loadRocketLaunches()
print(launches)
}
EXECUTION DIAGRAM
ONE TASK
Task closure begins
│
▼
Enter loadRocketLaunches()
│
▼
Enter downloadLaunchData()
│
▼
Possible suspension
│
▼
Return through loadRocketLaunches()
│
▼
Continue the Task closure
The function calls form one logical task unless the code explicitly creates additional tasks using features such as async let, a task group or another Task.
💡 Remember
An async function describes suspendible work.
A task provides the running lifetime in which that asynchronous function executes.
05 · TUTORIAL
A Task Executes in Several Periods
Synchronous code encourages us to imagine one function occupying its thread until it returns.
CODE EXAMPLE
func loadLocalLaunches() throws -> [Launch] {
let data = try Data(contentsOf: launchesFileURL)
return try decodeLaunches(from: data)
}
Once the thread enters this synchronous function, it remains occupied by the function until the call returns or throws.
An asynchronous task can have a different shape:
CODE EXAMPLE
func loadRemoteLaunches() async throws -> [Launch] {
let (data, _) = try await URLSession.shared.data(
from: launchesURL
)
return try decodeLaunches(from: data)
}
The task executes synchronously until it reaches an operation that genuinely needs to wait.
If the task suspends, its current period of execution ends.
EXECUTION DIAGRAM
ONE TASK, TWO PERIODS OF EXECUTION
PERIOD 1
████ begin URLSession request ████
│
▼
SUSPEND
│
│ no thread is reserved
│ merely to preserve the wait
│
▼
RESULT BECOMES READY
│
▼
PERIOD 2
████ receive data → decode → return ████
The second period may run on the same thread or a different thread. Swift does not require the task's complete lifetime to remain attached to one thread.
The task preserves the logical operation while threads provide temporary execution resources.
06 · TUTORIAL
What Happens at await?
await marks a possible suspension point.
CODE EXAMPLE
let launches = try await loadRemoteLaunches()
Execution reaches the asynchronous call.
If the called operation can continue immediately, the task may continue without suspending.
If the operation must wait, the task can suspend and allow its current thread to execute other eligible work.
EXECUTION DIAGRAM
REACH await
│
├── Result is ready
│ │
│ └── Continue executing
│
└── Result is not ready
│
└── Suspend the task
│
└── Continue later
await does not mean “move this function to a background thread.”
It does not guarantee suspension. It identifies a place where suspension is permitted because asynchronous work may not yet be complete.
07 · TUTORIAL
A Task Is Not a Thread
A task describes work. A thread executes instructions.
CODE EXAMPLE
TASK
Logical operation:
Load the latest rocket launches
THREAD
Temporary execution resource:
Runs the task's current machine instructions
One task can execute during several periods and potentially use different threads across those periods.
One thread can also execute portions of many different tasks over time.
EXECUTION DIAGRAM
ONE SYSTEM THREAD
TIME
│
▼
Task A instructions ███████
Task B instructions █████
Task C instructions ███████
Task A continues █████
This conceptual schedule shows concurrency on one thread. Only one task's instructions execute at a particular instant, but several tasks make progress during the same period.
💡 Important
Task {} does not promise a new thread.
It creates a unit of asynchronous work whose eligible portions are scheduled by Swift.
08 · TUTORIAL
Task {} Inherits Its Surrounding Context
A task created with Task {} begins with important parts of the surrounding context, including actor isolation, priority and task-local values.
This matters when the task is created from a main-actor-isolated feature:
CODE EXAMPLE
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var status = "Waiting"
func beginRefresh() {
Task {
status = "Loading"
performExpensiveSynchronousCalculation()
status = "Finished"
}
}
}
The new task inherits the surrounding main-actor isolation.
Its synchronous calculation still executes as main-actor work. Wrapping that calculation in Task {} does not make it background work.
EXECUTION DIAGRAM
MAIN-ACTOR METHOD
│
▼
Create Task {}
│
▼
Task inherits MainActor isolation
│
▼
Synchronous calculation occupies
the main execution domain
This is valuable for UI work because the task can safely access the feature's isolated state.
It is not a performance escape hatch.
09 · TUTORIAL
The Value Returned by Task {} Is a Handle
Task {} immediately returns a value representing the created task.
CODE EXAMPLE
let task = Task {
try await loadRocketLaunches()
}
The task can begin running independently of whether we keep this value.
Keeping the handle gives us ways to interact with the task:
CODE EXAMPLE
let task = Task {
try await loadRocketLaunches()
}
let launches = try await task.value
Accessing value waits asynchronously for the task to complete and returns its success value. If the task can fail, accessing the value can throw.
The handle can also request cancellation:
CODE EXAMPLE
task.cancel()
If the handle is discarded, the task does not automatically stop. We simply lose this direct way to await its result or cancel it later.
10 · TUTORIAL
Cancellation Is a Request
Calling cancel() marks the task as cancelled.
It does not forcibly terminate whichever instruction happens to be executing.
CODE EXAMPLE
let task = Task {
for launch in launches {
try Task.checkCancellation()
await process(launch)
}
}
task.cancel()
The task cooperates by checking its cancellation state at suitable points.
Task.checkCancellation() throws CancellationError when cancellation has been requested. Code can instead inspect Task.isCancelled when it needs to return partial work or perform its own cleanup.
EXECUTION DIAGRAM
Cancellation requested
│
▼
Task cancellation flag becomes true
│
▼
Task reaches a cancellation-aware operation
or explicitly checks its state
│
▼
Task chooses how to stop
Some asynchronous APIs respond to cancellation themselves. Our own long-running loops and multi-stage operations still need deliberate cancellation behaviour.
11 · TUTORIAL
A Feature Should Own the Tasks It Starts
The runtime manages execution, but it does not know whether a result is still useful to the user.
That is an architectural decision.
A launch-list feature can own the lifetime of its refresh task:
CODE EXAMPLE
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
@Published private(set) var isLoading = false
private let api: LaunchAPI
private var refreshTask: Task<Void, Never>?
init(api: LaunchAPI) {
self.api = api
}
func refresh() {
refreshTask?.cancel()
refreshTask = Task {
isLoading = true
defer { isLoading = false }
do {
launches = try await api.loadLaunches()
} catch where Task.isCancelled {
// This refresh no longer owns the visible result.
} catch {
launches = []
}
}
}
func stop() {
refreshTask?.cancel()
refreshTask = nil
}
}
The API owns the capability to load launches.
The feature owns the task because the feature knows when a refresh begins, when an older refresh becomes obsolete and when the result is no longer needed.
Starting a second refresh cancels the first task before creating a new one.
Stopping the feature requests cancellation of the task whose lifetime belonged to it.
💡 The Architectural Rule
The code that understands why a task exists should normally control how long that task is allowed to live.
12 · TUTORIAL
Task {} Is Unstructured Concurrency
A task created directly with Task {} is an unstructured task.
It can inherit context from the code that creates it, but it is not automatically a structured child whose lifetime is bounded by the surrounding function.
This flexibility is useful at synchronous boundaries such as a button handler:
CODE EXAMPLE
Button("Refresh") {
feature.refresh()
}
The synchronous action cannot directly call and await an asynchronous operation, so the feature creates a task and explicitly owns its handle.
Inside an existing asynchronous operation, structured forms are often a better description of work that must complete before the parent operation finishes:
CODE EXAMPLE
func loadDashboard() async throws -> Dashboard {
async let launches = api.loadLaunches()
async let agencies = api.loadAgencies()
return try await Dashboard(
launches: launches,
agencies: agencies
)
}
The two async let operations are structured child tasks. Their lifetimes remain inside loadDashboard().
That full parent-and-child model belongs to the next article: What Is Structured Concurrency?
13 · TUTORIAL
The Complete Mental Model
EXECUTION DIAGRAM
FEATURE
│
│ creates and owns
▼
TASK
│
├── created
├── runnable
├── running
├── suspended
├── runnable again
└── completed or cancelled
│
│ eligible periods become jobs
▼
EXECUTOR
│
│ arranges execution using
▼
SYSTEM THREADS
│
▼
PROCESSOR CORES
A task gives one asynchronous operation a logical identity and lifetime.
Its code executes synchronously between possible suspension points. When it genuinely needs to wait, the task can suspend without reserving its current thread.
When the awaited operation becomes ready, the task becomes eligible to continue through an appropriate executor.
The task is not the thread that temporarily executes it. It is the operation that persists across those separate periods of execution.
That is a Swift Task.
14 · TUTORIAL
What to Remember
💡 What to Remember
1. A Swift Task is a unit of asynchronous work managed by the Swift Concurrency runtime.
2. All asynchronous Swift code executes as part of a task.
3. Calling an async function does not automatically create another task.
4. A task can execute in several periods separated by suspension.
5. await marks a possible suspension point. It is not a command to change threads.
6. A task is not a thread and does not receive its own dedicated thread.
7. Task {} inherits important surrounding context, including actor isolation.
8. The value returned by Task {} is a handle used to await a result or request cancellation.
9. Cancellation is cooperative. It does not forcibly terminate the task.
10. A feature should own the lifetime of the unstructured tasks it creates.
15 · TUTORIAL
Frequently Asked Questions
What is a Task in Swift?
A Task is one logical unit of asynchronous work managed by Swift. It can contain several periods of execution, suspend while waiting and later continue through an appropriate executor.
Does Task {} create a new thread?
No. Task {} creates asynchronous work, not an operating-system thread. Swift arranges for eligible parts of the task to execute using system threads underneath.
Does an async function create a task?
Not by itself. Calling an asynchronous function normally continues the current task into that function. New tasks are introduced by task-creation constructs such as Task {}, async let and task groups.
Does a Task begin immediately?
A task becomes eligible to run when it is created and can begin before the following code has progressed very far. The exact scheduling order is not a guarantee developers should use for coordination.
Does a Task stop when its handle is released?
No. Discarding the task handle does not cancel the task. The task continues, but the caller loses that handle's ability to await its value or request cancellation.
Does task.cancel() immediately stop a Task?
No. Cancellation marks the task as cancelled. The task and the asynchronous operations it calls must cooperate by checking or responding to that cancellation state.
What is the difference between Task and Task.detached?
Task {} inherits important surrounding context such as actor isolation, priority and task-local values. Task.detached creates more independent unstructured work without inheriting that actor context. Detached tasks should not be used as a generic way to make code safe or move every calculation into the background.
16 · TUTORIAL
Continue Learning Swift Concurrency
Read Swift Concurrency Runtime to revisit the scheduling system that manages tasks.
Continue with What Is Structured Concurrency? to learn how Swift gives tasks parent-and-child relationships and prevents child work from escaping its intended lifetime.
For the language contract, consult Swift's official Concurrency documentation and Apple's Task reference.
17 · TUTORIAL
Download Xcode Playground
The accompanying What Is a Task in Swift? Xcode playground can make the complete task lifetime visible.
It should create one task, observe its synchronous execution periods, suspend it with Task.sleep, await its result, cancel a long-running operation and demonstrate that releasing a task handle does not cancel the task.
A final page should place the task inside LaunchListFeature so the reader can connect runtime behaviour to feature ownership.
The article defines the task. The playground will let the reader watch one live from creation to completion.
