What Is Sendable in Swift?
Sendable is a Swift protocol that marks a type whose values can safely cross concurrency boundaries without introducing data races.
struct Launch: Sendable {
let id: UUID
let name: String
let date: Date
}
A Launch value can be passed into an actor, returned from a task or moved between other isolation domains because all of the state it contains is safe to share in concurrent code.
The central idea: actor isolation protects state inside a boundary. Sendable describes values that are safe to carry across that boundary.
Why Swift Needs Sendable
Consider a normal class containing mutable launch information:
final class LaunchDraft {
var name: String
init(name: String) {
self.name = name
}
}
A class instance is a reference. Two concurrent tasks can receive the same reference and attempt to change the same property.
let draft = LaunchDraft(name: "Artemis II")
Task {
draft.name = "Artemis 2"
}
Task {
draft.name = "Artemis II Mission"
}
The value crossing into each task contains access to unprotected mutable state. Passing the reference did not create independent state for each task.
Swift therefore needs a type-level answer to this question: if a value leaves its current concurrency domain, can code in another domain use it safely?
What Is a Concurrency Domain?
A concurrency domain is a region of code and data whose mutable state has one isolation context. A task, an actor instance and a global actor such as MainActor can form boundaries between concurrent work.
The following is a conceptual diagram:
Launch API task ── Launch value ──▶ LaunchStore actor
LaunchStore actor ── snapshot ──▶ MainActor feature
The launch values cross between domains. Sendable expresses that these crossings do not carry unsafe shared mutable state with them.
Sendability is the property of being safe to pass or share across concurrency domains.
Sendable Is a Marker Protocol
Sendable does not require methods or properties.
struct Launch: Sendable {
let id: UUID
let name: String
let date: Date
}
The conformance is a semantic contract. It states that every value of this type can be used across concurrency boundaries without exposing unprotected mutable state.
The compiler checks whether the type’s stored state supports that promise. Conformance is therefore not decorative documentation. It participates in Swift’s data-race safety checking.
Value Types Are Often Naturally Sendable
Structures and enumerations are strong candidates for Sendable because passing a value does not normally give another domain unrestricted access to the same mutable storage.
enum LaunchStatus: Sendable {
case scheduled
case delayed(reason: String)
case launched
}
struct Launch: Sendable {
let id: UUID
let name: String
let date: Date
let status: LaunchStatus
}
Every stored property and associated value must also be sendable. The safety of the outer type is built from the safety of the values inside it.
Launch
├─ UUID Sendable
├─ String Sendable
├─ Date Sendable
└─ LaunchStatus Sendable
Result: Launch can conform to Sendable
var Does Not Automatically Make a Struct Non-Sendable
A sendable structure can contain variable properties.
struct LaunchProgress: Sendable {
var completedStages: Int
var totalStages: Int
}
LaunchProgress remains a value type. One task can change its own value without necessarily changing another task’s value.
This is why “sendable means immutable” is too narrow. Immutability is one straightforward path to safety, but independent value semantics can also make mutable values safe to transfer.
Every Stored Value Must Support the Contract
A structure cannot safely claim sendability if it contains a reference to unprotected mutable state.
final class LaunchNotes {
var text = ""
}
struct Launch: Sendable {
let id: UUID
let notes: LaunchNotes
// Error: LaunchNotes is not Sendable.
}
Copying the Launch structure copies the notes reference. Two domains could then reach the same mutable LaunchNotes instance.
Task A ─▶ Launch value A ─┐
├─▶ same mutable LaunchNotes
Task B ─▶ Launch value B ─┘
The outer structure’s value semantics cannot repair unsafe reference semantics hidden inside it.
Reference Types Require Greater Care
A class can conform to Sendable, but its design must make sharing the same instance safe.
final class LaunchConfiguration: Sendable {
let agencyName: String
let requestTimeout: Duration
init(agencyName: String, requestTimeout: Duration) {
self.agencyName = agencyName
self.requestTimeout = requestTimeout
}
}
This final class contains only immutable sendable state. Sharing its reference does not allow either caller to mutate the instance.
A class with ordinary mutable properties cannot make the same promise:
final class LaunchCounter: Sendable {
var count = 0
// Error: mutable stored property in a Sendable class.
}
Reference types need immutability, actor or global-actor isolation, or correctly implemented synchronization to make shared mutation safe.
Actors Are Sendable
An actor reference can cross concurrency boundaries because access to its mutable state remains protected by actor isolation.
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
}
}
func importLaunch(
_ launch: Launch,
into store: LaunchStore
) async {
await store.save(launch)
}
Passing store does not expose its array. Every caller still has to cross the actor’s isolation boundary to use the protected state.
This reveals an important distinction: sendability does not require a value to be copied or immutable. A shared reference can be sendable when its mutable state is safely isolated.
Global-Actor Isolation Can Make a Type Safe to Share
A type isolated to MainActor also protects its mutable state behind an isolation boundary.
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
func display(_ launches: [Launch]) {
self.launches = launches
}
}
Code can carry a reference to the feature, but it cannot freely mutate the isolated state from another domain. It must enter MainActor.
func show(
_ launches: [Launch],
in feature: LaunchListFeature
) async {
await feature.display(launches)
}
The safety comes from isolation. It does not mean the object is freely mutable from every thread.
Sendable Does Not Mean Thread-Safe in Every Possible Use
The phrase “thread-safe” can suggest that any operation may be performed from any thread without further rules. Sendable is more precise: values of the type can cross concurrency domains without introducing data races.
An actor reference is sendable, but its isolated properties still cannot be accessed directly. A main-actor-isolated reference is safe to pass, but its isolated operations still require MainActor. Sendability preserves these access rules; it does not remove them.
Sendable permits a value to cross a boundary. It does not grant the receiver unrestricted access to everything behind that value.
Sendable Does Not Create Runtime Synchronization
Declaring conformance does not insert a lock, create an actor or schedule work on a serial executor.
struct Launch: Sendable {
let id: UUID
let name: String
}
No runtime manager is attached to Launch. The compiler verifies the type’s structure and allows it to be used where sendability is required.
The work happens mainly at compile time:
Type declares Sendable
│
▼
Compiler checks stored state
│
├─ safe ─────▶ crossing is permitted
│
└─ unsafe ───▶ diagnostic at compile time
Implicit Sendable Conformance
Swift can infer sendability for some value types when their stored data is sendable and the declaration is not exposed across certain resilience boundaries.
struct LaunchCoordinate {
let latitude: Double
let longitude: Double
}
This simple internal structure can be treated as sendable without writing the conformance explicitly.
Explicit conformance is still valuable when sendability is part of the type’s intended API:
struct LaunchCoordinate: Sendable {
let latitude: Double
let longitude: Double
}
Now adding a non-sendable stored property can produce a diagnostic at the type declaration, where the contract is easiest to understand.
Generic Types Need Sendable Ingredients
A generic container is only sendable when the values stored inside it are sendable.
struct APIResponse<Value: Sendable>: Sendable {
let value: Value
let receivedAt: Date
}
The constraint connects the outer guarantee to its generic argument. An APIResponse<Launch> can be sendable because Launch is sendable. The type does not promise safety for arbitrary unknown state.
What @unchecked Sendable Means
Some reference types protect mutable state using synchronization that the compiler cannot verify. Swift allows the programmer to take responsibility with @unchecked Sendable.
final class LockedLaunchCounter: @unchecked Sendable {
private let lock = NSLock()
private var value = 0
func increment() {
lock.withLock {
value += 1
}
}
func currentValue() -> Int {
lock.withLock {
value
}
}
}
The compiler accepts the conformance without proving that every access is synchronized. The programmer is promising that the implementation protects all shared mutable state correctly.
@unchecked Sendable should therefore describe a safety guarantee that already exists. It should not be added merely to silence a concurrency warning.
@unchecked Sendable means the type claims the full Sendable contract, but the compiler is not responsible for verifying the implementation.
A Complete Feature Boundary
The launch feature can now be described as values moving between protected domains:
struct Launch: Sendable {
let id: UUID
let name: String
let date: Date
}
actor LaunchStore {
private var launches: [Launch] = []
func replace(with launches: [Launch]) {
self.launches = launches
}
func snapshot() -> [Launch] {
launches
}
}
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
func refresh(from store: LaunchStore) async {
launches = await store.snapshot()
}
}
LaunchStore protects its mutable array. LaunchListFeature protects visible state on MainActor. The Launch values forming the snapshot are sendable, so they can safely cross between those isolation domains.
LaunchStore actor
│
│ [Launch] is Sendable
▼
MainActor feature
This is the architectural role of Sendable: isolation establishes ownership, while sendable values form safe messages between owners.
The Complete Mental Model
When a value crosses into a task or actor, reason through its stored state:
Value crosses a concurrency boundary
│
▼
Could it expose shared mutable state?
│
┌───────┴────────┐
│ │
no yes
│ │
▼ ▼
Value semantics, Is mutation protected
immutability or by actor isolation or
safe composition synchronization?
│ │
└───────┬────────┘
▼
Sendable contract
Sendable does not specify whether safety comes from copying, immutable state or protected shared state. It specifies the result: crossing the boundary cannot introduce a data race.
What to Remember
Sendablemarks types whose values can safely cross concurrency boundaries.- It is a compile-time semantic contract with no method requirements.
- Value types are sendable when the state they contain is sendable.
- A
varproperty does not automatically make a value type non-sendable. - Embedding an unsafe mutable reference can make an otherwise simple structure non-sendable.
- Reference types require immutability, isolation or correct synchronization.
- Actors are sendable because access to their mutable state remains isolated.
- Sendability does not remove actor or global-actor access rules.
Sendabledoes not create runtime locking or scheduling.@unchecked Sendabletransfers the verification responsibility to the programmer.
Frequently Asked Questions
Does Sendable mean a value is immutable?
No. Immutability is one way to satisfy the contract. Independent value semantics and properly protected shared state can also be sendable.
Does Sendable copy a value when it crosses an actor boundary?
No. Sendable does not define a copying operation. Normal value and reference semantics still apply.
Are all structs automatically Sendable?
No. A structure can contain a non-sendable reference or another unsafe value. Swift infers conformance only in contexts where all required conditions are satisfied.
Can a class conform to Sendable?
Yes, but sharing one reference must remain safe. A final class with immutable sendable state is a straightforward example. Mutable classes need an enforceable protection strategy.
Why is an actor Sendable if it contains mutable state?
Because passing the actor reference does not bypass actor isolation. All isolated access to its mutable state remains coordinated through the actor.
Should I add @unchecked Sendable to remove a warning?
No. Use it only when the type already implements synchronization that genuinely satisfies the contract and you are prepared to maintain that guarantee manually.
Continue Learning
Sendable describes values that can cross concurrency boundaries. Closures can cross those boundaries too, and a closure may carry captured state with it. The next article, What Is @Sendable in Swift?, will explain how Swift checks sendable function types and the values captured by their closures.
Download the Xcode Playground
Use the accompanying playground to send Launch values between a task, LaunchStore and a main-actor feature. Add a non-sendable reference property to observe the compiler diagnostic, repair the model using value semantics and examine a synchronized class whose safety requires @unchecked Sendable.
