← All writing
HackerNoonApril 2025

Mastering Swift Structured Concurrency

Structured concurrency changed the shape of asynchronous Swift: complex flows read top to bottom instead of nesting into callbacks. This is a working tour of the machinery underneath that — starting with Task as the way into an async context from synchronous code, and .task as its SwiftUI counterpart tied to a view's lifetime.

The bulk of it is about cancellation, which is where most real code is wrong: how to stay cancellation-aware, what withTaskCancellationHandler is for, and why swallowing cancellation with try? is a bug rather than a tidy-up. It also covers task priorities, the difference between .task(priority:) and Task.detached(priority:), and awaiting results through .value.

A closing section collects the traps that show up in real-world code: wrapping a cancellable call in try? and losing the thrown CancellationError along with it, letting a task's thrown error disappear instead of surfacing it, and reading a priority hint as a promise about which actor or thread the work actually runs on.

It is explicit about its own scope: child tasks, task groups, task-local values and custom executors are pointed to as the next things to explore rather than folded into this piece.

What's inside

  • How Task opens an async context from ordinary synchronous code and inherits the actor it was created on, which is why a task started on MainActor stays there unless told otherwise.
  • The .task view modifier as SwiftUI's way of tying async work to a view's lifetime, including how changing a .task(id:) value cancels the running instance and starts a fresh one.
  • Why cancellation in Swift is cooperative rather than automatic: marking a task cancelled only sends a signal, and code has to check for it or rely on a throwing suspension point to actually stop.
  • withTaskCancellationHandler as the tool for reacting to cancellation immediately — useful when a task needs to run cleanup the moment it's told to stop, rather than waiting for the next suspension point.
  • The gap between a task's priority and its execution context: priorities like .background are scheduling hints, not a way to move work off the MainActor, which is what Task.detached is actually for.
  • A pitfalls list drawn from real-world code — try? erasing a cancellation error, a thrown error going unhandled, and priority being mistaken for thread placement.
The takeaway

The piece's core claim is that async/await solves the readability problem but not the discipline problem: most of what separates working structured-concurrency code from broken code is how carefully it treats cancellation as cooperative and priority as a hint rather than a guarantee.

Get the full withTaskCancellationHandler and Task.detached code on HackerNoon ↗