Swift Sendable and Isolation Boundaries

Transfer appropriate values across concurrency boundaries, keep mutable state with its actor owner, and interpret strict concurrency diagnostics.

Lesson 40 of 43Advanced10 min read

Concurrency safety depends on what crosses a boundary and who may mutate it afterward. Sendable describes values that can be transferred safely under Swift's concurrency model. Actor isolation keeps mutable operations with a designated owner. These tools address data races, while application rules such as stale results still need separate logic.

The concurrency lesson mentions Sendable. This reference develops that boundary into practical design. Swift 6 language mode strengthens concurrency checking. Toolchain version, language mode and isolation settings are distinct; newer toolchains can support migration modes and configurable default isolation, so record the project's settings when interpreting diagnostics.

Transfer values with a clear ownership story

An immutable struct containing Sendable fields is a natural snapshot to transfer. String and Int provide suitable value semantics for these examples. A struct containing a reference to a mutable non-Sendable class does not become safe merely because the outer declaration is a value type.

Explicit Sendable conformance asks the compiler to check the applicable rules. Public API and representation details can affect implicit conformance, so make the intent visible for a shared DTO. A final class with only appropriate immutable stored state may satisfy checked conformance, while a mutable class generally needs isolation or a real synchronization design.

Unchecked Sendable is a promise made by the author without the same compiler proof. It does not install a lock or repair unsafe access. Use it only when a thoroughly reviewed implementation actually provides the required safety, and document that synchronization contract. Adding the annotation merely to silence a warning hides the problem rather than resolves it.

Closures carry captured state across boundaries

A Sendable closure can be invoked in a concurrency context that requires safe capture behavior. Capturing an immutable Sendable value differs from capturing an unprotected mutable variable or a reference whose owner can change it concurrently. The compiler's diagnostic often points to that capture rather than to the closure's business calculation.

Task inheritance and Task.detached have different context relationships. A detached task does not inherit actor isolation in the same way as an ordinary task created within an actor context. Do not switch to detached merely because an actor-related diagnostic appears; it can widen the boundary and expose more unsafe state.

Prefer capturing a needed snapshot or invoking an actor-owned operation over capturing a whole mutable controller. A closure that needs only a code string should not retain every field and resource of an unrelated UI object. Narrow captures make both sendability and memory lifetime easier to review.

Actor isolation is access control for concurrent state

An actor serializes access to its isolated mutable state under the language model. Calls from outside require the appropriate asynchronous access, while code within the actor can work with its state directly. Global actors such as MainActor assign declarations to a shared isolation domain, commonly used for UI-owned state.

Await is a potential suspension point, not a universal command to move work to a particular thread. An actor can permit other work while one operation is suspended, so Sendable and isolation do not make a multi-step business invariant automatically survive an await. Keep state transitions together or revalidate after suspension when needed.

Nonisolated declarations must not access isolated mutable state as though they still owned it. Returning a Sendable snapshot from an actor is often clearer than exposing the actor's mutable implementation objects. Cross the boundary with the data a consumer needs, then leave updates with the actor that owns them.

Diagnose the boundary rather than suppress the message

When a diagnostic mentions sending, isolation or a capture, identify the source owner, destination context and value being transferred. Ask whether the value is immutable, independently copied, isolated or synchronized. This produces a concrete fix instead of a collection of annotations whose contracts contradict one another.

Migration tools and preconcurrency imports can help stage adoption of older APIs, but they do not prove an external library is race-free. Keep compatibility accommodations narrow and follow the library's documented threading behavior. Test concurrent behavior, and review default actor isolation settings when a toolchain upgrade changes diagnostics.

Data-race checking also does not decide business freshness. Two individually safe requests can finish in the wrong order and publish an obsolete response. Use cancellation and generation checks for that policy, as developed in the actor reentrancy reference. Compiler safety is a foundation for application correctness, not a substitute for every domain invariant.

Official references

Read the Swift 6 concurrency migration guide, Sendable proposal and Swift concurrency guide. A useful review names transferred values, mutable owners and suspension boundaries rather than simply counting concurrency annotations.

Swift Sendable and Isolation Boundaries Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Transfer an immutable message

The detached closure captures a value with an explicit transfer contract. Detachment is used only to demonstrate a boundary; ordinary application code should prefer structured ownership where it fits. Waiting for the value makes this console example finite. Compile the standalone async entry point with swiftc -parse-as-library Demo.swift.

Swift
struct Message: Sendable {
    let code: String
    let quantity: Int
}

@main struct Demo {
    static func main() async {
        let input = Message(code: "PEN", quantity: 3)
        let task = Task.detached { input }
        let output = await task.value
        print("\(output.code):\(output.quantity)")
    }
}
Output
PEN:3

Swift Sendable and Isolation Boundaries in Real Projects

Worked exercise: Return a stock snapshot

Keep stock mutation inside an actor and transfer an immutable snapshot to callers. Reserve only an available positive quantity. The method performs its check and mutation without an internal suspension, preserving the local invariant.

Swift
struct StockSnapshot: Sendable {
    let code: String
    let remaining: Int
}
actor Stock {
    private let code: String
    private var units: Int
    init(code: String, units: Int) { self.code = code; self.units = units }
    func reserve(_ quantity: Int) -> Bool {
        guard quantity > 0, quantity <= units else { return false }
        units -= quantity
        return true
    }
    func snapshot() -> StockSnapshot { StockSnapshot(code: code, remaining: units) }
}

@main struct Demo {
    static func main() async {
        let stock = Stock(code: "PEN", units: 7)
        print(await stock.reserve(3))
        let value = await stock.snapshot()
        print("\(value.code):\(value.remaining)")
    }
}
Output
true
PEN:4

The example starts from a known valid initial count. Add explicit initial-value validation for uncertain inputs. The snapshot exposes information rather than the actor's mutable fields. A database-backed reservation would need its own transaction contract; local actor isolation does not coordinate every external client.

Swift Sendable and Isolation Boundaries FAQ

Does Sendable mean an object is immutable?

Not universally. Immutable value models are a straightforward way to satisfy the contract, but actors and properly synchronized types can support safe concurrent use through other rules. The important point is that transfer must not create unsafe unsynchronized sharing. A mutable reference inside a struct still needs attention. Examine the relevant fields and ownership rather than treating the Sendable name as proof that every possible operation is unrestricted or free of business-level races.

Is unchecked Sendable a quick migration fix?

It is an author-supplied safety promise, not a synchronization mechanism. A real lock or ownership design may justify it for an implementation the compiler cannot verify, but adding it to an unsafe mutable class simply suppresses useful checking. Document the protected fields, every access path and the synchronization rules, then review them. Prefer checked value conformance or actor isolation when those designs express the intended contract directly.

Does MainActor make every asynchronous operation instantaneous?

No. It establishes an isolation domain for owned state, not a guarantee of speed or absence of suspension. An async operation can wait for external work and permit other actor work while suspended. Keep expensive blocking computation off latency-sensitive UI paths through an appropriate design, and revalidate state after await when needed. MainActor helps govern safe access; it does not automatically solve stale results, cancellation policy or responsiveness.

Why do diagnostics change after a toolchain upgrade?

Compiler checks, language modes and default isolation settings can evolve, and a newer toolchain can offer several migration configurations. Inspect the project's actual mode and build settings before interpreting a changed warning or error. The underlying ownership question remains useful: what is sent, from which owner to which context? Avoid copying a version-specific workaround without understanding that boundary, and consult the official migration guide for the supported adoption path.

Swift Sendable and Isolation Boundaries Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A UI closure needs only a product code from a mutable controller. Which capture is easier to review?

Swift Sendable and Isolation Boundaries Quiz

2 questions to check what you learned.

  1. Which transfer design is straightforward for these examples?

    • An unprotected mutable reference shared everywhere.
    • An immutable Sendable snapshot with Sendable fields.
    • unchecked Sendable added without any safety design.
  2. What does unchecked Sendable do?

    • Automatically creates a mutex around every field.
    • Prevents all stale network responses.
    • Relies on the author's safety promise rather than installing synchronization.