ZombieMutex
All writing
Engineering#iOS#Swift#Swift Concurrency#Async Await#Reliability
November 6, 20264 min read

Swift concurrency cancellation is cooperative, not an automatic rollback

How Task.checkCancellation, child tasks, stale results, and server-side effects interact in an iOS app.

By

Share

Send this post or copy the direct link.

An iOS view disappears while a network call is still running. The app cancels its task. Does that guarantee the server's operation stops?

No. Swift cancellation is cooperative. Cancellation requests propagate through structured child tasks, but arbitrary work does not stop until code observes and responds to the cancelled state. And cancelling a client task cannot safely be treated as undoing a server-side mutation.

The Swift language concurrency reference describes both Task.checkCancellation and Task.isCancelled. These are useful because a cancellation signal has meaning only when work handles it.

Cancellation and completion are different outcomes

A task can be cancelled and still execute code before it reaches another suspension point or cancellation check. A callback might produce a result late, after the screen has moved to a different user or a newer query.

If the UI displays the late result without checking whether the request still belongs to the active state, the user can see stale content even when the task was otherwise well-behaved.

There are at least three boundaries:

  1. The task boundary — the unit of asynchronous work that can observe cancellation.
  2. The UI ownership boundary — whether the result still belongs to the current screen and input.
  3. The server side-effect boundary — whether a remote action already committed despite client cancellation.

Each one needs its own policy.

A simple cancellation-aware transformation

Consider a CPU-heavy conversion function invoked from an async context. Check cancellation between meaningful units of work rather than assuming a single check at the start is enough.

swift
func transform(items: [String]) async throws -> [String] {
    var output: [String] = []
    output.reserveCapacity(items.count)

    for item in items {
        try Task.checkCancellation()
        output.append(item.trimmingCharacters(in: .whitespacesAndNewlines))
    }

    return output
}

This example is intentionally small. For truly CPU-intensive work, monitor cancellation at a granularity that balances overhead against how quickly work should stop. For network requests, prefer native cancellation-aware APIs and propagate the cancellation through the call chain rather than swallowing CancellationError.

A request token can prevent stale UI updates

Suppose a user searches for "Swift" and then immediately searches for "Kotlin". The first request may return after the second. Even if the first task is cancelled, it is safer to associate every result with the generation or request identifier that created it.

Conceptually, the UI should accept a response only when it is still the response for the latest input and the current view owner. That's a UI consistency guard, not authentication.

For stateful work, the generation should be owned by the appropriate actor or main-actor-isolated view model. Avoid sharing a mutable counter across concurrent tasks without synchronization.

Error handling should preserve cancellation

If every error is mapped to “please retry” and blindly retried, cancellation becomes indistinguishable from a transient transport failure. That can lead to repeated work after navigation.

Handle cancellation as cancellation. Distinguish it from network timeouts, 4xx validation errors, authentication failures, and 5xx service errors. A generic catch-and-retry loop without an overall deadline can outlive the screen that initiated it.

Cancellation is not transaction rollback

A POST request can be accepted, committed, and acknowledged too late for the app to observe the result. Cancelling the Swift task or its underlying transport does not prove that the server discarded the request.

The API idempotency simulator demonstrates one such case with synthetic timing, and the companion article explains why a server-enforced operation key or status endpoint matters.

If a client action affects money, accounts, access rights, or device ownership, make the backend operation safe to retry independently of cancellation behavior in Swift.

Tests that expose the gaps

  • Cancel a task before its first suspension point and during a long computation.
  • Navigate away while a request is in flight, then confirm no stale result overwrites the new view.
  • Issue overlapping requests with different query parameters.
  • Disconnect after a server mutation but before receiving the response.
  • Confirm background cancellation does not trigger unbounded automatic retries.
  • Distinguish user-initiated cancellation from token expiry and server failures.

Apple's Task cancellation reference is explicit: cancellation does not forcibly stop arbitrary functions. A cancelled task may run to completion if it never checks its status.


Editorial note: Reference tutorial prepared with AI assistance. The illustrative code is not a complete app or a verified production benchmark.

Related technical notes with overlapping topics.