Course outline

iOS / Swift / Concurrency and data boundaries / Lesson 7

Async, await, and structured cancellation

Understand suspension and cooperative cancellation before connecting tasks to a screen lifetime.

About 5 min readContent reviewed
On this page

Lesson goals

  • Async describes suspension, not a background thread
  • Structured children belong to their scope
  • Await independent synthetic results

Async describes suspension, not a background thread

An async function can suspend at an await point and resume later. It does not promise that all work inside it runs on a background thread or that a CPU-heavy loop becomes responsive automatically. Executor behavior depends on isolation annotations, the selected Swift language mode, and project settings that can evolve across toolchains. State those assumptions when explaining performance or compiler diagnostics. Keep UI-facing mutable state explicitly main-actor isolated in the course examples. Treat every await as a boundary where other work may run and where the input or owner can become obsolete.

Structured children belong to their scope

An async let binding starts a child task whose lifetime is bounded by its surrounding scope. A task group similarly organizes related child work and makes completion and error policy explicit. Use concurrency when operations can proceed independently, not when the second operation requires the first result. Creating many unstructured Tasks is not equivalent to structured concurrency: their handles and cancellation responsibilities must be managed separately. Keep task inputs small and safe to transfer across isolation boundaries. A concurrency diagnostic about Sendable should prompt an ownership review rather than an unchecked annotation added merely to silence the compiler.

Await independent synthetic results

The first fragment combines two local asynchronous functions. async let expresses that neither label depends on the other; awaiting the tuple collects both values in a chosen result order. The output order is therefore Maps, Rivers even if the underlying operations complete in another order. These immediate functions illustrate structure, not a measured speedup. Replacing them with real I/O requires its own failure, cancellation, and resource policy. The fragment deliberately avoids an application entry point and does not claim a Swift compiler run in the authoring environment.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
func firstLabel() async -> String { "Maps" }
func secondLabel() async -> String { "Rivers" }
func combinedLabels() async -> String {
    async let first = firstLabel()
    async let second = secondLabel()
    let values = await (first, second)
    return "\(values.0), \(values.1)"
}
// await combinedLabels() -> "Maps, Rivers"
Expected output
Maps, Rivers

The tuple order is deterministic; scheduling and actual parallel execution are not asserted.

Cancellation requires cooperation

Cancellation marks a task as cancelled; it does not forcibly stop arbitrary instructions. The second fragment checks cancellation before work and after a suspending sleep. Task.sleep can throw on cancellation, so allowing that error to propagate preserves the caller’s cancellation policy. Do not catch it and continue as if successful unless that behavior is intentional and documented. Long CPU loops need suitable checkpoints or another cancellation design. For a remote command, cancellation of local waiting does not prove the server rolled back a change already accepted.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
func delayedLabel() async throws -> String {
    try Task.checkCancellation()
    try await Task.sleep(nanoseconds: 1_000_000)
    try Task.checkCancellation()
    return "Maps"
}
// A non-cancelled caller receives "Maps".
// A cooperating cancelled operation throws instead of publishing a label.
Expected output
A non-cancelled call returns Maps; cancellation follows the throwing path.

The delay is a tiny synthetic interval, not a retry recommendation or timing assertion. No task was executed.

Attach tasks to an explicit lifetime

SwiftUI task modifiers can connect asynchronous work to view activity and restart work for an identified input. Their cancellation signal still needs cooperation from the operation. A task retained by a model has a different owner and may intentionally outlive one view. Decide which scope creates the handle, cancels it, and consumes its result. Avoid a detached task as a routine escape from actor errors; it changes inheritance and transfer requirements. Make cancellation, stale-result rejection, and error presentation separate decisions so ending one screen cannot accidentally publish into a new interaction.

Test cancellation at controlled boundaries

Use a fake operation that pauses at a known point, cancel its owner, then release it and verify the documented outcome. Avoid a real-time sleep as the only proof that cancellation happened. Test a normal result, an ordinary failure, cancellation before work begins, and cancellation while awaiting. For a UI task, separately verify navigation away and a changed input cannot commit obsolete state. The course’s expected task outcomes remain illustrative because Swift and Xcode are unavailable. A passing pure cancellation test in another host would not establish device background execution or remote rollback.

Common errors and fixes

An async function still blocks interaction

Why it happens
Synchronous CPU work was mistaken for automatically background execution.
How to fix it
Inspect isolation and measure the expensive work before selecting a concurrency design.
Prevent it next time
Explain suspension separately from parallelism.

Cancelled work still publishes success

Why it happens
Cancellation was ignored or its error swallowed.
How to fix it
Check cancellation and preserve the intended propagation policy.
Prevent it next time
Test a controlled cancellation path.

Detached tasks hide transfer warnings

Why it happens
Task creation was used instead of resolving ownership and isolation.
How to fix it
Choose an explicit owner and transfer safe values across boundaries.
Prevent it next time
Review Sendable diagnostics as design evidence.

Check your understanding

1. Does cancelling a task prove a remote reservation was not saved?

Show worked solution

No. It can stop or abandon local work, while the server may already have accepted the command. Preserve an operation identifier and reconcile uncertain completion according to the service contract. A local cancellation result is not a transaction rollback guarantee.

2. When should two asynchronous operations remain sequential?

Show worked solution

When the second needs a value, authorization decision, or side effect produced by the first. Starting both with async let would violate that dependency. Express concurrency only for genuinely independent work and keep the expected result ordering explicit.

Key takeaways

  • Async describes suspension, not a background thread
  • Test cancellation at controlled boundaries

Official references