top of page

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.


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.

bottom of page