Why was async/await (SE‑0296) introduced instead of extending existing callback patterns, and how does it relate to the exclusive‑access model and result builders (SE‑0289) in practice?

Commonplace

Why Swift moved to async/await instead of “just” extending the old callback style

  1. Callbacks were hard to read and error‑prone – The proposal points out that the existing callback‑based APIs required “deeply‑nested closures” (the classic “pyramid of doom”) and made error handling noisy with many guard or do‑catch blocks [[p:6e9396b4-43f9-4d56-9558-262a2414f43b] §0].
  2. The goal was a linear, “synchronous‑looking” style – By introducing the async keyword, a function can be written in the same control‑flow style as a normal function, letting the compiler insert the necessary suspension points instead of the programmer manually chaining callbacks [[p:2dc5a9df-34ae-45d8-8c60-15db1238bf0e] §1], [[p:68a159fc-b4e3-4ba2-a978-3e1ebba22c1a] §3].
  3. Error handling becomes straightforward – With async/await the same try/catch syntax used for synchronous code works for asynchronous code, eliminating the need for a separate Result‑based error‑propagation pattern that still required nested do‑catch blocks [[p:5f6d0ada-7c6c-4495-bd27-fdb08b5530a9] §5].
  4. Debugging and correctness improve – The proposal notes that forgetting to call a completion handler was a common source of bugs, and that the new model forces the programmer to write an explicit await at every possible suspension point, making it obvious where the thread can be released [[p:8d3a6cb5-bee7-498f-9b7a-0ff7444781fc] §8].
  5. Explicit suspension points preserve atomicity – By requiring await to be written at each place where a function may suspend, the language guarantees that code without an await runs atomically, preventing interleaving that could otherwise cause data‑race‑like bugs (e.g., double‑spending, UI flicker) [[p:49099c40-6140-4b5b-9edd-5007584e696e] §11]. Together these points explain why the Swift team chose a new language construct rather than trying to retrofit the existing callback pattern. ---

How async/await fits with the exclusive‑access model (Sendable/actors) and with result builders

Exclusive‑access model (the “Sendable” / actor isolation story) is about guaranteeing that values moved across concurrency domains are safe. The async/await proposal was deliberately designed to work hand‑in‑hand with that model:

  • The compiler treats a suspension point (await) as a potential domain hop. Because the point is explicit, the compiler can enforce that any value captured across the hop conforms to Sendable (or is otherwise protected by an actor), preserving the exclusive‑access guarantees [[p:11d005f9-a96e-4aac-91f5-756481f6bd70] §0] (SE‑0302) and the rule that suspension points must be marked [[p:49099c40-6140-4b5b-9edd-5007584e696e] §11] (SE‑0296).
  • Actors themselves are implicitly Sendable, so an await that sends a message to an actor automatically respects the exclusive‑access contract [[p:20e0c516-0373-4068-b570-17e09f7ea6b7] §20] (SE‑0302). In practice this means you can write: ``swift await myActor.doWork() // safe – the actor’s mailbox guarantees exclusive access `` without any extra boilerplate. Result builders (SE‑0289) are a compile‑time transformation that lets you write DSL‑style code (e.g., SwiftUI view trees) with a clean syntax. They are additive – they do not change the runtime semantics of the program and therefore do not interfere with the concurrency model [[p:29b4c13c-ea3a-4691-8871-fb662a9401e8] §29]. In practice:
  • A result‑builder‑generated function can be async, and the builder’s generated code will contain the same explicit await points required by the async/await model. The builder itself does not need to know about concurrency; the compiler simply applies the two transformations sequentially.
  • Because result builders preserve local declarations (they are not stripped away) [[p:13bca494ed-2671-42f7-8fa5-7963c2228c2e] §13], any values captured inside a builder’s closure are subject to the same Sendable checks as any other closure. If a builder captures a non‑Sendable reference and the resulting closure is marked @Sendable (or is passed across an actor boundary), the compiler will emit an error, keeping the exclusive‑access guarantees intact. In short, async/await was introduced to give developers a first‑class way to express asynchronous work that is both readable and safe. Its design deliberately exposes suspension points, which lets the compiler enforce the exclusive‑access model (Sendable/actors). Result builders remain a separate, compile‑time DSL mechanism that can be used inside async contexts without breaking those safety guarantees.