01 · INTRODUCTION
What Is Task Cancellation in Swift?
The short answer
Task cancellation is Swift’s cooperative mechanism for telling asynchronous work that its result is no longer needed. Cancelling a task marks it as cancelled; the task and the functions it calls must notice that state and stop appropriately.
CODE EXAMPLE
let refreshTask = Task {
try await launchManager.refresh()
}
refreshTask.cancel()
The call to cancel() sends a cancellation signal. It does not forcibly terminate the task at that exact line, kill a thread or erase the work already performed.
The central idea: cancellation changes what a task should do next. The task must cooperate by observing cancellation and finishing promptly.
02 · TUTORIAL
The Real Problem Is Obsolete Work
Imagine that the user opens the Rocket Launch app and pulls to refresh. Before the first request finishes, they refresh again.
EXECUTION DIAGRAM
Refresh A ─────────────── returns old result
Refresh B ─── returns new result
If refresh B finishes first, the feature displays the newest launches. Refresh A can then finish later and overwrite that state with an older response.
CODE EXAMPLE
Task {
let launches = try await manager.fetchLaunches()
self.launches = launches
}
The code is free from a low-level data race when it runs on MainActor, but it still has a logical race. Two valid operations complete in an order that no longer matches the user’s intent.
Cancellation lets the feature declare that refresh A became obsolete when refresh B began.
03 · TUTORIAL
Cancellation Is a State, Not an Interruption
A task has cancellation state that can be queried while the task is running.
CODE EXAMPLE
if Task.isCancelled {
return
}
Once marked cancelled, the task remains cancelled. The runtime does not jump into arbitrary synchronous code and force it to return.
EXECUTION DIAGRAM
Running task
│
│ cancel() called
▼
Task marked cancelled
│
│ task reaches a check or cancellation-aware await
▼
Task responds and finishes
This cooperative design gives each operation control over safe cleanup and the meaning of a partial result.
Cooperative cancellation means cancellation is requested by setting task state, while the running operation remains responsible for observing and responding to that request.
04 · TUTORIAL
Task.isCancelled Lets Code Choose a Normal Return
Use Task.isCancelled when cancellation should produce a normal result or an early return.
CODE EXAMPLE
func normalise(
_ launches: [Launch]
) -> [Launch] {
var result: [Launch] = []
for launch in launches {
if Task.isCancelled {
return result
}
result.append(launch.normalised())
}
return result
}
This function chooses to return the successfully processed values. That can be appropriate for best-effort work, but it also hides the distinction between a complete result and a cancelled partial result.
The response to cancellation is an API design decision. Returning an empty array, a partial array or no value can each be correct in a different operation.
05 · TUTORIAL
Task.checkCancellation Throws
Use Task.checkCancellation() when cancellation should stop the current operation by throwing CancellationError.
CODE EXAMPLE
func normalise(
_ launches: [Launch]
) throws -> [Launch] {
var result: [Launch] = []
for launch in launches {
try Task.checkCancellation()
result.append(launch.normalised())
}
return result
}
The thrown error travels through ordinary Swift error handling. Callers cannot mistake the partial array for a completed answer.
CODE EXAMPLE
do {
let launches = try normalise(rawLaunches)
await store.replace(with: launches)
} catch is CancellationError {
// The result became obsolete. Do not publish it.
}
06 · TUTORIAL
Many Suspending APIs Already Check Cancellation
Some Swift concurrency APIs respond to cancellation while suspended. Task.sleep, for example, throws when its task is cancelled.
CODE EXAMPLE
func searchLaunches(
matching query: String
) async throws -> [Launch] {
try await Task.sleep(for: .milliseconds(300))
return try await launchAPI.search(query)
}
The sleep creates a short debounce period. If the user types another character and the current search task is cancelled, the sleep can throw instead of waiting for the full delay.
Do not assume that every asynchronous function checks cancellation. An API must document or implement how it responds. Cancellation state automatically propagating into a task is not the same as every underlying operation stopping automatically.
07 · TUTORIAL
Check Before Publishing a Result
An operation can become cancelled while an awaited API is finishing. A final check protects the boundary where background work becomes visible feature state.
CODE EXAMPLE
func loadLaunches() async throws -> [Launch] {
let launches = try await launchAPI.fetchUpcomingLaunches()
try Task.checkCancellation()
return launches
}
The check says that a technically successful network response is not a valid result if the owning task no longer needs it.
The most important cancellation check is often the one immediately before obsolete work would update current state.
08 · TUTORIAL
A Feature Can Own Its Refresh Task
An unstructured task needs an explicit owner. A main-actor feature can retain its current refresh task and cancel it before starting another.
CODE EXAMPLE
@MainActor
final class LaunchFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
@Published private(set) var isLoading = false
@Published private(set) var errorMessage: String?
private let manager: LaunchManager
private var refreshTask: Task<Void, Never>?
private var activeRefreshID: UUID?
init(manager: LaunchManager) {
self.manager = manager
}
func refresh() {
refreshTask?.cancel()
let refreshID = UUID()
activeRefreshID = refreshID
isLoading = true
refreshTask = Task {
do {
let loaded = try await manager.fetchLaunches()
try Task.checkCancellation()
guard activeRefreshID == refreshID else { return }
launches = loaded
errorMessage = nil
} catch is CancellationError {
// A newer refresh owns the screen now.
} catch {
if activeRefreshID == refreshID {
errorMessage = "Could not load launches."
}
}
if activeRefreshID == refreshID {
isLoading = false
}
}
}
}
The task belongs to LaunchFeature, not to the short call of refresh(). Starting a newer refresh cancels the older task. The final check prevents the older task from publishing after cancellation.
Cancellation is treated as expected control flow rather than a user-facing network failure.
09 · TUTORIAL
Search Is the Clearest Cancellation Example
Every new character can make the previous search query obsolete.
CODE EXAMPLE
@MainActor
final class LaunchSearchFeature: ObservableObject {
@Published private(set) var results: [Launch] = []
private var searchTask: Task<Void, Never>?
private let api: LaunchAPI
func search(for query: String) {
searchTask?.cancel()
guard !query.isEmpty else {
results = []
return
}
searchTask = Task {
do {
try await Task.sleep(for: .milliseconds(300))
let matches = try await api.search(query)
try Task.checkCancellation()
results = matches
} catch is CancellationError {
// A newer query replaced this one.
} catch {
guard !Task.isCancelled else { return }
results = []
}
}
}
}
If the user types art, then quickly changes it to artemis, the task for art is no longer allowed to update the result list.
EXECUTION DIAGRAM
"art" task ───── cancelled ──X
"artemis" task ─────────────▶ displayed results
10 · TUTORIAL
Navigation Can Cancel Feature Work
When the user leaves a launch-details screen, its unfinished request may no longer have an owner.
CODE EXAMPLE
@MainActor
func didLeaveScreen() {
detailsTask?.cancel()
detailsTask = nil
}
The task still needs to cooperate, but the feature has made its lifecycle decision explicit. Work started for one screen should not continue updating that screen after the user has moved elsewhere.
Framework-managed task scopes can also connect cancellation to view lifetime. The architectural principle remains the same: the lifecycle that owns the result should own cancellation.
11 · TUTORIAL
Cancellation Propagates Down Structured Task Trees
When a parent task is cancelled, its async let and task-group children are marked cancelled too.
EXECUTION DIAGRAM
Cancelled refresh task
├─ SpaceX child marked cancelled
├─ NASA child marked cancelled
└─ ESA child marked cancelled
Each child still decides how quickly it stops. A provider waiting in a cancellation-aware operation may throw promptly. A long synchronous decoding loop must check cancellation itself.
CODE EXAMPLE
for try await providerResult in group {
try Task.checkCancellation()
launches += providerResult
}
The group does not abandon its children. Its structured scope waits for every child to finish responding before it exits.
12 · TUTORIAL
Child Cancellation Does Not Automatically Cancel Its Parent
Cancellation normally propagates down the task tree, from parent to children. A child deciding to finish early does not automatically mark its parent or siblings cancelled.
A parent can choose a broader policy. For example, after receiving the first acceptable provider result, it can call group.cancelAll() to tell the remaining children that their values are no longer required.
CODE EXAMPLE
if let firstResult = try await group.next() {
group.cancelAll()
return firstResult
}
The parent remains responsible for defining what one child’s completion or failure means for the overall operation.
13 · TUTORIAL
Cancellation Does Not Guarantee Network Abortion
Cancelling the Swift task may cause a cancellation-aware networking API to cancel its underlying request. That behaviour belongs to the API implementation; it is not guaranteed merely because a task has been marked cancelled.
A wrapper around a callback-based or custom network client may need to connect Swift cancellation to the underlying request explicitly.
CODE EXAMPLE
func fetchData(
using request: NetworkRequest
) async throws -> Data {
try await withTaskCancellationHandler {
try await request.value()
} onCancel: {
request.cancel()
}
}
This conceptual wrapper assumes that NetworkRequest is safe to share and that cancel() can be called synchronously from the handler.
14 · TUTORIAL
Cancellation Handlers Connect External Resources
withTaskCancellationHandler registers an onCancel closure around an operation.
CODE EXAMPLE
try await withTaskCancellationHandler {
try await operation.run()
} onCancel: {
operation.requestStop()
}
The handler runs when cancellation is requested, including when the task was already cancelled before entering the handler’s scope. It does not replace cooperative checks inside the operation.
The cancellation handler is synchronous and may run concurrently with the operation. It should perform small, thread-safe notification or cleanup work. It should not directly mutate ordinary task-local or actor-isolated state without the correct protection.
15 · TUTORIAL
Cancellation and Errors Have Different Meanings
A failed provider and a cancelled refresh are not the same event.
• A provider error means the requested operation was still wanted but could not complete.
• Cancellation means the operation’s result is no longer wanted, or its owner is ending.
CODE EXAMPLE
do {
launches = try await manager.fetchLaunches()
} catch is CancellationError {
return
} catch {
errorMessage = "Could not load launches."
}
Treating cancellation as an ordinary failure can flash unnecessary error messages whenever a user types, refreshes again or leaves a screen.
16 · TUTORIAL
defer Still Performs Local Cleanup
Cancellation often exits through a thrown error, so ordinary Swift cleanup remains useful.
CODE EXAMPLE
func refresh() async throws {
isLoading = true
defer { isLoading = false }
launches = try await manager.fetchLaunches()
}
The defer block runs whether loading succeeds, fails or throws because cancellation was detected. Use cancellation handlers to notify external concurrent resources; use defer for predictable lexical cleanup owned by the function.
17 · TUTORIAL
The Complete Mental Model
EXECUTION DIAGRAM
Owner no longer needs result
│
▼
task.cancel()
│
▼
Task marked cancelled; handlers notified
│
▼
Task reaches check or aware suspension point
│
┌────┴────┐
▼ ▼
return throw CancellationError
│ │
└────┬────┘
▼
Cleanup runs; obsolete result is not published
Cancellation state can flow down a structured task tree, but each operation must cooperate. The owner initiates cancellation, the task detects it, lower-level resources receive it when necessary and the feature refuses stale results.
18 · TUTORIAL
What to Remember
• Cancellation is a cooperative signal, not forced task termination.
• cancel() marks a task as cancelled and runs registered cancellation handlers.
• Task.isCancelled supports a normal early return.
• Task.checkCancellation() throws CancellationError.
• Some suspending APIs detect cancellation, but not every asynchronous API does.
• Check cancellation before publishing a result that may have become obsolete.
• The feature lifecycle that owns a task’s result should own its cancellation.
• Structured cancellation propagates from parents to children and remains cooperative.
• Cancelling one child does not automatically cancel its parent or siblings.
• Task cancellation does not inherently guarantee cancellation of an underlying network request.
• Cancellation handlers bridge cancellation to thread-safe external resources.
• Cancellation is expected control flow and should not automatically become a user-facing error.
19 · TUTORIAL
Frequently Asked Questions
Does task.cancel() stop a task immediately?
No. It marks the task as cancelled. The task stops when its code or a called API observes cancellation and responds.
Does cancellation kill the task’s thread?
No. Tasks do not permanently own threads, and cancellation does not terminate a system thread.
What is the difference between isCancelled and checkCancellation()?
isCancelled returns a Boolean so the operation can choose its response. checkCancellation() throws CancellationError when the current task is cancelled.
Will await automatically throw when a task is cancelled?
No. Only cancellation-aware throwing operations do so. await by itself is merely a potential suspension marker.
Should cancellation display an error?
Usually not when cancellation represents normal user behaviour such as replacing a search, refreshing again or leaving a screen.
Where should I check cancellation?
Check inside substantial synchronous loops, after important suspension points and immediately before publishing a result whose relevance may have changed.
20 · TUTORIAL
References
21 · TUTORIAL
Continue Learning
The refresh and search examples retain a Task whose lifetime extends beyond the synchronous method that created it. The next article, What Is an Unstructured Task in Swift?, will explain who owns that task, which context it inherits and how to manage its handle, result and cancellation without losing the guarantees our architecture now depends on.
22 · TUTORIAL
Download the Xcode Playground
Use the accompanying playground to start two overlapping launch refreshes and observe the stale result overwrite the newer one. Retain and cancel the first task, add cancellation checks before state publication, then implement a debounced launch search and a custom request bridged through a cancellation handler.
