Chapter 13 — Isolation in depth
Isolation is a property of every declaration
- isolated to a specific actor instance (a method on an
actor), - isolated to a global actor (
@MainActor func), - or nonisolated (belongs to no actor — the default for free functions and value-type methods).
nonisolated, precisely
actor Job: CustomStringConvertible {
let id: Int
var progress = 0
init(id: Int) { self.id = id }
// Must be nonisolated to satisfy the synchronous protocol requirement.
nonisolated var description: String { "Job(\(id))" } // can only touch immutable `id`
}
Isolated parameters: "run me on this actor"
// This function runs isolated to whatever actor you hand it.
func addEntry(to cache: isolated ImageCache, url: URL, image: UIImage) {
cache.insert(image, for: url) // no await! we're isolated to `cache` here
}
Region-based isolation: the compiler tracks where values go
func rehome() async {
let chicken = Chicken() // non-Sendable
await island.adopt(chicken) // ✅ crossing allowed: `chicken` is never used again here
// If we touched `chicken` after this line, the compiler would reject the crossing.
}
flowchart LR
C["create non-Sendable value"] --> SEND["send it to another domain"]
SEND --> Q{"used again in
the sender?"} Q -->|"No"| OK["✅ it was a transfer — safe"] Q -->|"Yes"| BAD["❌ it's shared — rejected"]
the sender?"} Q -->|"No"| OK["✅ it was a transfer — safe"] Q -->|"Yes"| BAD["❌ it's shared — rejected"]
sending: transfer across a boundary by contract
// Without sending: ERROR — ColorComponents is non-Sendable, crossing to @MainActor.
func updateStyle(backgroundColor: ColorComponents) async {
await applyBackground(backgroundColor) // ❌ risks a data race
}
// With sending: the parameter is transferred; the caller can't use it afterward → safe.
func updateStyle(backgroundColor: sending ColorComponents) async {
await applyBackground(backgroundColor) // ✅ proven safe: it's a hand-off, not sharing
}
The four fixes for a boundary-crossing error
flowchart TB
ERR["'sending value risks data races'"] --> F1["1. Make the type Sendable
(if it can be a value type / immutable)"] ERR --> F2["2. Mark the parameter `sending`
(if it's a genuine transfer)"] ERR --> F3["3. Isolate the whole function
(so there's no boundary to cross)"] ERR --> F4["4. Use a @Sendable closure
(compute the value in the target domain)"]
(if it can be a value type / immutable)"] ERR --> F2["2. Mark the parameter `sending`
(if it's a genuine transfer)"] ERR --> F3["3. Isolate the whole function
(so there's no boundary to cross)"] ERR --> F4["4. Use a @Sendable closure
(compute the value in the target domain)"]
- Make the type
Sendable. If it's really a value or can be made immutable, this is the cleanest — - Mark the parameter
sending. If the value is genuinely handed off and not reused, this expresses - Isolate the whole function. If the "boundary" only exists because your function is
nonisolated - Use a
@Sendableclosure. Instead of sending a value in, pass a closure that produces the
nonisolated(unsafe): the storage escape hatch
// A global the compiler would otherwise demand be Sendable / isolated.
nonisolated(unsafe) var legacyGlobalCache: [String: Data] = [:]
Reading isolation errors fluently
- What is the isolation of the code I'm in? (nonisolated?
@MainActor? some actor?) - What boundary is being crossed, and in which direction? (into an actor, into a
Task, to - Is the value being shared or transferred? If transferred,
sendingor region analysis should - Pick from the four fixes based on whether the type can be
Sendable, whether it's a genuine
What we built in this chapter
- Established that every declaration has an isolation (some actor, a global actor, or
nonisolated), - Introduced isolated parameters (
isolated ImageCache) for functions that run *on a passed-in - Explained region-based isolation — the compiler proves a non-
Sendablevalue is transferred, - Laid out the four principled fixes for a boundary-crossing error (make it
Sendable, mark it - Turned all of it into a method for reading isolation errors: identify your isolation, the boundary
Mental model to take away
- Isolation is a property of every declaration. The annotations (
nonisolated,isolated, - Region-based isolation means Swift 6 allows many non-
Sendablecrossings it can prove are - A crossing error has four principled fixes (Sendable / sending / isolate the function / Sendable
- Read every isolation error as **"which value crosses which boundary, in which direction, shared or