Sendable
Sendable
Section titled “Sendable”Use this when:
- A value or reference type must cross an isolation boundary safely.
- You are resolving “non-Sendable type” compiler diagnostics.
- You need to decide between value types,
@unchecked Sendable, actors, or region-based isolation.
Skip this file if:
- The issue is about which actor should own the state. Use
actors.md. - The issue is about how async functions execute. Use
threading.md.
Jump to:
- Isolation Domains
- Value Types (Structs, Enums)
- Reference Types (Classes)
- Functions and Closures (@Sendable)
- @unchecked Sendable
- Region-Based Isolation /
sending - Global Variables
- Decision Tree
What is Sendable?
Section titled “What is Sendable?”Sendable indicates a type is safe to share across isolation domains (actors, tasks, threads). The compiler verifies thread-safety at compile time.
public protocol Sendable {}Empty protocol, but triggers compiler verification of thread-safety.
Isolation Domains
Section titled “Isolation Domains”Three types of isolation in Swift Concurrency:
1. Nonisolated (default)
Section titled “1. Nonisolated (default)”No concurrency restrictions, but can’t modify isolated state:
func computeValue(a: Int, b: Int) -> Int { return a + b}2. Actor-isolated
Section titled “2. Actor-isolated”Dedicated isolation domain with serialized access:
actor Library { var books: [String] = []
func addBook(_ title: String) { books.append(title) }}
// External access requires awaitawait library.addBook("Swift Concurrency")3. Global actor-isolated
Section titled “3. Global actor-isolated”Shared isolation domain across types:
@MainActorfunc updateUI() { // Runs on main thread}Data Races vs Race Conditions
Section titled “Data Races vs Race Conditions”Data Race
Section titled “Data Race”Multiple threads access shared mutable state, at least one writes, without synchronization:
// ⚠️ Data racevar counter = 0DispatchQueue.global().async { counter += 1 }DispatchQueue.global().async { counter += 1 }Detection: Enable Thread Sanitizer in scheme settings.
Prevention: Use actors or Sendable types:
actor Counter { private var value = 0
func increment() { value += 1 }}Race Condition
Section titled “Race Condition”Timing-dependent behavior leading to unpredictable results:
let counter = Counter()
for _ in 1...10 { Task { await counter.increment() }}
// May print inconsistent valuesprint(await counter.getValue())Key difference: Swift Concurrency prevents data races but not race conditions. You must still ensure proper sequencing.
Value Types (Structs, Enums)
Section titled “Value Types (Structs, Enums)”Implicit conformance
Section titled “Implicit conformance”Non-public structs/enums with Sendable members:
// Implicitly Sendablestruct Person { var name: String}Explicit conformance required
Section titled “Explicit conformance required”Public types need explicit declaration:
public struct Person: Sendable { var name: String}Why: Compiler can’t verify internal details of public types across modules.
Frozen types
Section titled “Frozen types”Public frozen types can be implicitly Sendable:
@frozenpublic struct Point: Sendable { public var x: Double public var y: Double}All members must be Sendable
Section titled “All members must be Sendable”public struct Person: Sendable { var name: String var hometown: Location // Must also be Sendable}
public struct Location: Sendable { var name: String}Copy-on-write makes mutability safe
Section titled “Copy-on-write makes mutability safe”public struct Person: Sendable { var name: String // Mutable but safe due to COW}Each mutation creates a copy, preventing concurrent access to same instance.
Reference Types (Classes)
Section titled “Reference Types (Classes)”Requirements for Sendable classes
Section titled “Requirements for Sendable classes”Must be:
final(no inheritance)- Immutable stored properties only
- All properties Sendable
- No superclass or
NSObjectonly
final class User: Sendable { let name: String let id: Int
init(name: String, id: Int) { self.name = name self.id = id }}Why non-final classes can’t be Sendable
Section titled “Why non-final classes can’t be Sendable”Child classes could introduce unsafe mutability:
// Can't be Sendableclass Purchaser { func purchase() { }}
// Could introduce data racesclass GamePurchaser: Purchaser { var credits: Int = 0 // Mutable!}Actor isolation makes classes Sendable
Section titled “Actor isolation makes classes Sendable”@MainActorclass ViewModel { var data: [Item] = [] // Safe due to actor isolation}// Implicitly SendableComposition over inheritance
Section titled “Composition over inheritance”final class Purchaser: Sendable { func purchase() { }}
final class GamePurchaser { let purchaser: Purchaser = Purchaser() // Handle credits separately}Functions and Closures (@Sendable)
Section titled “Functions and Closures (@Sendable)”Mark functions/closures that cross isolation domains:
actor ContactsStore { func removeAll(_ shouldRemove: @Sendable (Contact) -> Bool) async { contacts.removeAll { shouldRemove($0) } }}Captured values must be Sendable
Section titled “Captured values must be Sendable”let query = "search"
// ✅ Immutable capturestore.filter { contact in contact.name.contains(query)}
var query = "search"
// ❌ Mutable capturestore.filter { contact in contact.name.contains(query) // Error}Capture lists for mutable values
Section titled “Capture lists for mutable values”var query = "search"
// ✅ Capture immutable snapshotstore.filter { [query] contact in contact.name.contains(query)}@unchecked Sendable
Section titled “@unchecked Sendable”Use as last resort. Tells compiler to skip verification—you guarantee thread-safety.
When to use
Section titled “When to use”Manual locking mechanisms the compiler can’t verify:
final class Cache: @unchecked Sendable { private let lock = NSLock() private var items: [String: Data] = [:]
func get(_ key: String) -> Data? { lock.lock() defer { lock.unlock() } return items[key] }
func set(_ key: String, value: Data) { lock.lock() defer { lock.unlock() } items[key] = value }}- No compile-time safety
- Easy to introduce data races
- Must manually ensure all access uses lock
final class Cache: @unchecked Sendable { private let lock = NSLock() private var items: [String: Data] = [:]
// ⚠️ Forgot lock - data race! var count: Int { items.count }}Better: Use actor instead:
actor Cache { private var items: [String: Data] = [:]
var count: Int { items.count }
func get(_ key: String) -> Data? { items[key] }
func set(_ key: String, value: Data) { items[key] = value }}Region-Based Isolation
Section titled “Region-Based Isolation”Compiler allows non-Sendable types in same scope:
class Article { var title: String init(title: String) { self.title = title }}
func check() { let article = Article(title: "Swift")
Task { print(article.title) // ✅ OK - same region }}Why: No mutation after transfer, so no data race risk.
Breaks when accessed after transfer
Section titled “Breaks when accessed after transfer”func check() { let article = Article(title: "Swift")
Task { print(article.title) }
print(article.title) // ❌ Error - accessed after transfer}The sending Keyword
Section titled “The sending Keyword”Enforces ownership transfer for non-Sendable types:
Parameter values
Section titled “Parameter values”actor Logger { func log(article: Article) { print(article.title) }}
func printTitle(article: sending Article) async { let logger = Logger() await logger.log(article: article)}
// Usagelet article = Article(title: "Swift")await printTitle(article: article)// article no longer accessible hereReturn values
Section titled “Return values”@SomeActorfunc createArticle(title: String) -> sending Article { return Article(title: title)}Transfers ownership to caller’s region.
Global Variables
Section titled “Global Variables”Must be concurrency-safe since accessible from any context.
Problem
Section titled “Problem”class ImageCache { static var shared = ImageCache() // ⚠️ Not concurrency-safe}Solution 1: Actor isolation
Section titled “Solution 1: Actor isolation”@MainActorclass ImageCache { static var shared = ImageCache()}Solution 2: Immutable + Sendable
Section titled “Solution 2: Immutable + Sendable”final class ImageCache: Sendable { static let shared = ImageCache()}Solution 3: nonisolated(unsafe)
Section titled “Solution 3: nonisolated(unsafe)”Last resort - you guarantee safety:
struct APIProvider: Sendable { nonisolated(unsafe) static private(set) var shared: APIProvider!
static func configure(apiURL: URL) { shared = APIProvider(apiURL: apiURL) }}Use private(set) to limit mutation points.
Custom Locks + Sendable
Section titled “Custom Locks + Sendable”Legacy code with locks
Section titled “Legacy code with locks”final class BankAccount: @unchecked Sendable { private var balance: Int = 0 private let lock = NSLock()
func deposit(amount: Int) { lock.lock() balance += amount lock.unlock() }
func getBalance() -> Int { lock.lock() defer { lock.unlock() } return balance }}Migration strategy
Section titled “Migration strategy”New code: Use actors
Existing code:
- If isolated and small scope → migrate to actor
- If widely used → use
@unchecked Sendable, file migration ticket
// Better: Migrate to actoractor BankAccount { private var balance: Int = 0
func deposit(amount: Int) { balance += amount }
func getBalance() -> Int { balance }}Decision Tree
Section titled “Decision Tree”Need to share type across isolation domains?├─ Value type (struct/enum)?│ ├─ Public? → Add explicit Sendable│ └─ Internal? → Implicit Sendable (if members Sendable)│├─ Reference type (class)?│ ├─ Can be final + immutable? → Sendable│ ├─ Needs mutation?│ │ ├─ Can use actor? → Use actor (automatic Sendable)│ │ ├─ Main thread only? → @MainActor│ │ └─ Has custom lock? → @unchecked Sendable (temporary)│ └─ Can be struct instead? → Refactor to struct│└─ Function/closure? → @Sendable attributeCommon Patterns
Section titled “Common Patterns”Restructure to avoid non-Sendable dependencies
Section titled “Restructure to avoid non-Sendable dependencies”// Instead of storing non-Sendable typepublic struct Person: Sendable { var hometown: String // Just the name
init(hometown: Location) { self.hometown = hometown.name }}Prefer actors for mutable state
Section titled “Prefer actors for mutable state”// Instead of @unchecked Sendable with locksactor Cache { private var items: [String: Data] = [:]
func get(_ key: String) -> Data? { items[key] }}Use @MainActor for UI-bound types
Section titled “Use @MainActor for UI-bound types”@MainActorclass ViewModel: ObservableObject { @Published var items: [Item] = []}Best Practices
Section titled “Best Practices”- Prefer value types - structs/enums are easier to make Sendable
- Use actors for mutable state - automatic thread-safety
- Avoid @unchecked Sendable - use only for proven thread-safe code
- Mark public types explicitly - don’t rely on implicit conformance
- Ensure all members Sendable - one non-Sendable breaks the chain
- Use @MainActor for UI types - simple isolation for view models
- Capture immutably - use capture lists for mutable variables
- Test with Thread Sanitizer - catches runtime data races
- File migration tickets - track @unchecked Sendable usage