Swift Sendable: a practical, step‑by‑step guide (2025)
Swift Sendable: a practical, step‑by‑step guide (2025)
Sendabletells 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
- What
Sendablemeans - Where it shows up in real code
- How Swift checks
Sendable - Using
@Sendablewith closures - Adopting
Sendable: step‑by‑step - Common errors & quick fixes
- Design tips and patterns
- Swift 6 notes (migration, “strict concurrency”, inference, and sending closures)
- FAQ
What Sendable means
- Formal idea: A type that conforms to
Sendablecan be passed between concurrent contexts (e.g., between tasks or across actor boundaries) without risking data races. The compiler enforces this at build time. (Swift.org) - Marker protocol:
Sendablehas no methods; it expresses a guarantee that the type is safe to share. (That’s why it’s often called a marker protocol.) The feature was introduced by SE‑0302 along with@Sendableclosures. (GitHub)
Mental model: If two pieces of code might run at the same time, values you pass between them must be either:
- Independent copies (typical for value types like
struct), or - Protected behind isolation (e.g.,
actor), or - Immutable and carefully designed (some
final classes), or - Explicitly marked “I know this is safe” using
@unchecked Sendable(escape hatch; use sparingly). (Apple Developer)
Where it shows up in real code
You’ll care about Sendable when you:
- Pass values across actor boundaries (e.g., from a UI‐isolated type to a background
actor). The values must be sendable. (Swift.org) - Create tasks or task groups and pass data into their work closures. Those closures are checked for safe captures (see below). (docs.swift.org)
- Write async APIs that accept or return values which might be used from other tasks. Declaring
Sendableconstraints makes intent clear and enforces safety in generic code. (Swift.org)
How Swift checks Sendable
Value types (struct, enum)
- Usually automatic. Value types get implicit
Sendableif all stored properties areSendable. You don’t even need to write: Sendablemost of the time. (Swift.org)
struct User /* : Sendable (implicit) */ {
let id: UUID // Sendable
var name: String // Sendable
}
Reference types (class)
- Classes aren’t value copies; they share state. So the compiler is stricter.
- You can conform a class to
Sendablewhen it is designed to avoid shared mutable state (e.g.,finaland effectively immutable, or its state is safely synchronized). - Special case:
@MainActorclasses are implicitly sendable, since the main actor serializes access to their state. (This is how many UI types remain usable with concurrency.) (Apple Developer)
@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
actorinstead (isolation guarantees safety by design), or use locks carefully with@unchecked Sendable(see below).
Actors
- Actor references are safe to pass around — they’re isolated; only one task at a time can touch their mutable state. This is why actors are a common fix for
Sendableproblems. (Actors are part of Swift’s data‑race safety model.) (Swift.org)
For types you don’t own
- The compiler needs to see all stored properties to verify safety, so checked conformance (
: Sendable) must be declared in the same file as the type. If you need to add conformance to a type in a different module/file, you must use@unchecked Sendableand take responsibility for safety. (Apple Developer)
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):
- They might be called from multiple threads at the same time, so what they capture must be safe to access concurrently.
- Therefore, the compiler forbids capturing mutable or non‑sendable values (e.g., a
classinstance that isn’tSendable). (docs.swift.org)
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
letconstant only helps if the type itself is Sendable. Writinglet counter = Counter()doesn’t make aclassmagically safe —letstops reassignment, not mutation.
Adopting Sendable: step‑by‑step
Follow this checklist to add Sendable safely and calmly.
Step 0 — Turn on the checks
- In Swift 6 language mode, data‑race safety (actor isolation + sendability) is enforced by default.
- In Swift 5 mode, you can enable complete concurrency checking as warnings via the
-strict-concurrency=completeflag (Xcode Build Settings orswiftc) or the equivalent Package.swift settings. This lets you fix issues before you flip your project to Swift 6 mode. (Swift.org)
Step 1 — Start with your data models
- Prefer value types (
struct/enum). They often become implicitlySendablewhen their properties are sendable. If you need to make it explicit (for public API clarity), writestruct Model: Sendable { ... }. (Swift.org)
Step 2 — Add constraints in generic code
- When writing generic async code, constrain parameters to
Sendableif they’ll cross concurrency boundaries:
func store<T: Sendable>(_ value: T) async { /* ... */ }
This makes intent clear and yields better diagnostics. (Swift.org)
Step 3 — Annotate closure parameters
- If a closure might be called from other tasks (e.g., background work, task groups), require
@Sendable:
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
- Inside
@Sendableclosures, don’t capture:- non‑sendable classes,
- mutable locals (
var), - or globals with shared mutable state.
- Solutions:
- Use an
actorto guard mutation. - Copy values into sendable forms (often by switching a small class to a
struct). - Limit scope so you pass only IDs/values, not whole objects. (docs.swift.org)
- Use an
Step 5 — For classes, pick one of these designs
- A. Make it an
actorif it owns mutable state shared by multiple tasks. - B. Keep it a
final classbut make state effectively immutable (allletstored properties of sendable types). - C. Use
@MainActorfor UI‑bound classes; they are implicitly sendable. (Apple Developer)
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
- Swift’s error messages point at the exact capture or property that breaks sendability (e.g., “captured var in
@Sendableclosure”). Fix them one by one.
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
- Favor values for data, actors for stateful services. Data models →
struct; shared mutable state →actor. This usually sidestepsSendableheadaches. (Swift.org) - Narrow what you pass. Pass IDs or small value snapshots instead of whole objects.
- Make generic APIs honest. If your API may be used from other tasks, add
: Sendableconstraints and@Sendableclosure parameters to catch mistakes earlier. (Swift.org) - Avoid the escape hatch unless necessary.
@unchecked Sendableis useful when wrapping legacy code with your own locking, but it becomes your permanent maintenance debt. (Apple Developer) - 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)
- Strict concurrency / data‑race safety is a key Swift 6 theme. You can try it as warnings in Swift 5 mode (
-strict-concurrency=complete) before upgrading. (Swift.org) - Inference improvements (SE‑0418). The compiler can infer
Sendablefor certain method references and key‑path literals so you don’t have to decorate everything by hand. You can also enable “Infer Sendable From Captures” in SwiftPM to reduce noise while migrating. (GitHub) sendingclosures (Swift 6): Some standard APIs (e.g.,Taskinitializers, task groups) now use sending closure parameters. A sending parameter transfers ownership so the caller can’t touch captured non‑sendable values after the call, which prevents races without requiring@Sendable. You’ll see diagnostics mentioning “sending closure risks causing data races” if you violate the rules. Think of it as a single‑transfer guarantee.@Sendablestill matters widely; sending is an additional tool. (GitHub)
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
- Enable checks (
-strict-concurrency=completein Swift 5 mode or switch to Swift 6 mode). Build and list all diagnostics. (Swift.org) - Tackle data models first. Convert obvious classes to
structoractor. Rebuild. (Swift.org) - Annotate APIs. Add
@Sendableto closure parameters that may run concurrently; add: Sendableconstraints to generics. (docs.swift.org) - Fix captures. Replace shared mutable objects with actors, or restructure to pass values/IDs. (docs.swift.org)
- Handle the stragglers. For types you don’t own, consider temporary
@unchecked Sendablewrappers until upstream libraries adopt sendability. Track these in code comments. (Apple Developer) - Re‑enable the strictest mode. Once clean, keep strict checks on so regressions are caught early. (Swift.org)
Further reading
Sendable(Apple Developer Docs) — definition, class rules,@unchecked Sendable, and notes about@MainActorclasses. (Apple Developer)- Swift 6 Concurrency Migration Guide: Data‑Race Safety — why value types are implicitly sendable and how checks work. (Swift.org)
- SE‑0302: Sendable and
@Sendableclosures — the original proposal. (GitHub) - Compiler diagnostics: captures in a
@Sendableclosure — concrete rules for what you can capture. (docs.swift.org) - SE‑0418: Inferring
Sendablefor methods and key‑path literals — reduces boilerplate and false positives in Swift 6. (GitHub) - Sending closures diagnostic / user docs — why some APIs use sending parameters and what those warnings mean. (docs.swift.org)
Wrap‑up
- Think “values & actors” for concurrent code.
- Use
@Sendableto make closure boundaries safe by default. - Reserve
@unchecked Sendablefor rare cases (and document them).