Swift Sendable: A Practical Step-by-Step Guide (2025)

Sendable tells the Swift compiler “this value is safe to share across concurrent code (different tasks/actors) without data races.” It’s a compile‑time safety net. (Swift.org)


Contents

  1. What Sendable means
  2. Where it shows up in real code
  3. How Swift checks Sendable
  4. Using @Sendable with closures
  5. Adopting Sendable: step‑by‑step
  6. Common errors & quick fixes
  7. Design tips and patterns
  8. Swift 6 notes (migration, “strict concurrency”, inference, and sending closures)
  9. FAQ

What Sendable means

Mental model: If two pieces of code might run at the same time, values you pass between them must be either:


Where it shows up in real code

You’ll care about Sendable when you:


How Swift checks Sendable

Value types (struct, enum)

struct User /* : Sendable (implicit) */ {
    let id: UUID       // Sendable
    var name: String   // Sendable
}

Reference types (class)

@MainActor
final class ImageCache {                // implicitly Sendable
    private var store: [URL: Data] = [:]
}

If you truly need a class that isn’t trivially safe, consider making it an actor instead (isolation guarantees safety by design), or use locks carefully with @unchecked Sendable (see below).

Actors

For types you don’t own


Using @Sendable with closures

Functions and closures can’t “conform” to protocols, so Swift uses an attribute:

func doWork(_ job: @escaping @Sendable () -> Void) { /* ... */ }

Rules for @Sendable closures (simplified):

Bad (captures a mutable class instance):

final class Counter { var value = 0 }
let counter = Counter()

func run(_ f: @escaping @Sendable () -> Void) { /* ... */ }

run {
    counter.value += 1     // ❌ capture of non-Sendable mutable state
}

Better (use an actor):

actor SafeCounter { private var value = 0; func inc() { value += 1 } }

let counter = SafeCounter()
run {
    Task { await counter.inc() }    // ✅ no shared mutable state
}

Tip: Capturing a let constant only helps if the type itself is Sendable. Writing let counter = Counter() doesn’t make a class magically safe — let stops reassignment, not mutation.


Adopting Sendable: step‑by‑step

Follow this checklist to add Sendable safely and calmly.

Step 0 — Turn on the checks

Step 1 — Start with your data models

Step 2 — Add constraints in generic code

func store<T: Sendable>(_ value: T) async { /* ... */ }

This makes intent clear and yields better diagnostics. (Swift.org)

Step 3 — Annotate closure parameters

func repeatAsync(times: Int, work: @escaping @Sendable () async -> Void) async {
    for _ in 0..<times { await work() }
}

This prevents callers from accidentally capturing unsafe values. (docs.swift.org)

Step 4 — Fix captures

Step 5 — For classes, pick one of these designs

Step 6 — Only if you must: @unchecked Sendable

If you interface with legacy or Objective‑C APIs and you know a type is safe (e.g., all access is behind a lock/queue), you can use:

extension FileHandle: @unchecked Sendable {}    // you must uphold safety

Warning: this skips compiler checks; you’re responsible for correctness. Prefer actor or value‑type refactors when possible. (Apple Developer)

Step 7 — Rebuild, read diagnostics, iterate


Common errors & quick fixes

Diagnostic (simplified) Why it happens Quick fix
“Capture of non‑Sendable type in a @Sendable closure” Closure might run concurrently; you captured a mutable/class value. Convert to struct/copy value, or use an actor, or make the class safely sendable. (docs.swift.org)
“Reference to captured var in concurrently executing code” Capturing a mutable local in a @Sendable closure is unsafe. Use a let copy, or wrap mutation in an actor. (docs.swift.org)
“Non‑Sendable type passed across actor boundary” You’re sending a value from one isolation to another. Make the type Sendable (value type), or use an actor. (Swift.org)
“Conformance to Sendable must be declared in the same file” The compiler needs full visibility of stored properties. Move the conformance next to the type. If you can’t, use @unchecked Sendable cautiously. (Apple Developer)
UI type can’t conform to Sendable UI classes are mutable and not thread‑safe. Mark the type @MainActor (implicitly sendable) and keep UI work on the main actor. (Apple Developer)

Design tips and patterns

  1. Favor values for data, actors for stateful services. Data models → struct; shared mutable state → actor. This usually sidesteps Sendable headaches. (Swift.org)

  2. Narrow what you pass. Pass IDs or small value snapshots instead of whole objects.

  3. Make generic APIs honest. If your API may be used from other tasks, add : Sendable constraints and @Sendable closure parameters to catch mistakes earlier. (Swift.org)

  4. Avoid the escape hatch unless necessary. @unchecked Sendable is useful when wrapping legacy code with your own locking, but it becomes your permanent maintenance debt. (Apple Developer)

  5. Know the “weird but true” bits.

    • Metatypes and some keypaths are considered sendable; Swift 6 also improves inference for method and key‑path references so you get fewer false warnings. (docs.swift.org)

Swift 6 notes (migration, “strict concurrency”, inference, and sending closures)


FAQ

Do I need to write : Sendable on every struct? No. Most value types become sendable implicitly when their stored properties are sendable. Add : Sendable when you want the guarantee to be part of your public API surface. (Swift.org)

Can classes be Sendable? Yes, but only when they can’t cause data races (e.g., final + immutable, or all access is synchronized). Otherwise, make them actors or keep them main‑actor‑isolated. (Apple Developer)

When is @unchecked Sendable OK? Only when you fully control access (e.g., all mutable state behind a lock/queue) and you’re willing to take responsibility if that changes later. Prefer safer designs first. (Apple Developer)

Why does my @Sendable closure reject var captures? Because it may run multiple times and concurrently. Capturing a var would allow racy mutation. Capture a let value or move the mutation into an actor. (docs.swift.org)


Worked examples

1) Making a model sendable (value type)

// Implicitly Sendable since all stored properties are sendable.
public struct TodoItem /* : Sendable */ {
    public let id: UUID
    public var title: String
    public var done: Bool
}

Why this works: value types are copied and don’t share mutable state across tasks. (Swift.org)


2) A generic async function that enforces Sendable

// Any value you "send" to the worker must be Sendable.
func runOnWorker<T: Sendable>(
    value: T,
    work: @escaping @Sendable (T) async -> Void
) async {
    await work(value)
}

This prevents callers from passing unsafe types or unsafe closures. (Swift.org)


3) Fixing a non‑sendable capture by using an actor

final class Metrics { var count = 0 }             // not sendable
let metrics = Metrics()

actor MetricsSink {                               // safe isolation
    private var count = 0
    func inc() { count += 1 }
}

let sink = MetricsSink()

func schedule(_ f: @escaping @Sendable () -> Void) { /* ... */ }

// ❌ Captures a class instance with shared mutable state.
schedule { metrics.count += 1 }

// ✅ Use the actor instead.
schedule { await sink.inc() }

4) Carefully using @unchecked Sendable for a wrapper

// Wrap a non-Sendable thing with explicit synchronization.
public final class LockedCounter: @unchecked Sendable {
    private var value = 0
    private let lock = NSLock()

    public func increment() {
        lock.lock(); defer { lock.unlock() }
        value += 1
    }
    public var current: Int {
        lock.lock(); defer { lock.unlock() }
        return value
    }
}

This compiles, but the safety is entirely your responsibility. Prefer actor unless you need Objective‑C interop or very specific performance behavior. (Apple Developer)


A quick migration recipe you can follow this week

  1. Enable checks (-strict-concurrency=complete in Swift 5 mode or switch to Swift 6 mode). Build and list all diagnostics. (Swift.org)
  2. Tackle data models first. Convert obvious classes to struct or actor. Rebuild. (Swift.org)
  3. Annotate APIs. Add @Sendable to closure parameters that may run concurrently; add : Sendable constraints to generics. (docs.swift.org)
  4. Fix captures. Replace shared mutable objects with actors, or restructure to pass values/IDs. (docs.swift.org)
  5. Handle the stragglers. For types you don’t own, consider temporary @unchecked Sendable wrappers until upstream libraries adopt sendability. Track these in code comments. (Apple Developer)
  6. Re‑enable the strictest mode. Once clean, keep strict checks on so regressions are caught early. (Swift.org)

Further reading


Wrap‑up