Chapter 12 — Sendable and the data-race model

Why crossing boundaries is dangerous

flowchart LR subgraph D1__Domain_A__a_background_task___ [\"Domain A (a background task)\"] A["var obj: MutableThing"] end subgraph D2__Domain_B__the_main_actor___ [\"Domain B (the main actor)\"] B["same MutableThing"] end A -->|"pass the reference"| B A -.->|"A still holds it,
mutates it"| SHARED["one object,
two domains mutating 💥"] B -.->|"B also mutates it"| SHARED

Sendable means: "a value of this type is safe to hand from one isolation domain to another without creating a data race." The compiler requires Sendable at every boundary crossing and refuses code that would send something unsafe.

What is Sendable, automatically

// Implicitly Sendable — a struct of Sendable members. Nothing to write.
struct Article {
    let id: UUID
    let title: String
    let publishedAt: Date
}
// Public API: state the guarantee explicitly.
public struct Article: Sendable {
    public let id: UUID
    public let title: String
}

Making a class Sendable

// Immutable and final → safe to send.
final class ImmutableConfig: Sendable {
    let baseURL: URL
    let timeout: TimeInterval
    init(baseURL: URL, timeout: TimeInterval) { self.baseURL = baseURL; self.timeout = timeout }
}

@unchecked Sendable: the escape hatch (handle with care)

// A class made thread-safe by a lock the compiler can't reason about.
final class Counter: @unchecked Sendable {
    private let lock = NSLock()
    private var _value = 0
    var value: Int { lock.withLock { _value } }
    func increment() { lock.withLock { _value += 1 } }
}
  • Use it only when you have real synchronization (a lock, a serial queue, atomics), not to silence an
  • Prefer an actor first — it gives you the same safety with compiler proof and usually less code.
  • Consider the Mutex type from the Synchronization module (Swift 6+) for a modern, Sendable-friendly

@Sendable closures

var counter = 0
Task {
    counter += 1      // ❌ error: mutable capture of `counter` in a @Sendable closure
}

Where the compiler checks Sendable

flowchart TB S["Sendable is required when a value…"] S --> A["…is passed to an actor method
(crosses into the actor)"] S --> B["…is returned from an actor method
(crosses out)"] S --> C["…is captured by Task / Task.detached"] S --> D["…is passed to / returned from a task group child"] S --> E["…crosses a global-actor hop (e.g. to @MainActor)"]

When a non-Sendable value legitimately needs to cross

What we built in this chapter

  • Defined Sendable as "safe to hand between isolation domains without a data race," and grounded it
  • Catalogued what's automatically Sendable (value types of Sendable members, Int/String/etc.,
  • Showed the two safe ways to make a class sendable (deeply immutable final, or **make it an
  • Explained @Sendable closures and their no-mutable-capture rule (the error you hit in
  • Signposted sending / region-based isolation for legitimately transferring a non-Sendable value

Mental model to take away

  • Sendable marks values safe to cross isolation boundaries. Values (copied) and actors (protected)
  • The compiler checks Sendable only at boundary crossings (into/out of actors, into tasks and task
  • @unchecked Sendable is a promise with no enforcement — every one is a place a race can hide. Use
  • When a non-Sendable value must legitimately move once, the answer is sending (next chapter),