Sendable & Data-Race Safety
Reviewed & published by Brayan K
Swift 6 makes data-race safety a compile-time guarantee. Learn the Sendable protocol, @Sendable closures, actor isolation, @MainActor, sending values across isolation domains, nonisolated, and why the compiler now enforces all of this.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • What the Sendable protocol means
- • Why value types are often Sendable automatically
- • What an @Sendable closure promises
- • Actor isolation and @MainActor
- • Opting out with nonisolated
- • Swift 6 strict concurrency checking at compile time
1️⃣ The Sendable Protocol
Sendable marks a type as safe to share across concurrency domains. Value types built from Sendable members conform automatically, so most structs are already safe.
// Sendable marks a type SAFE to share across concurrency domains
// (between tasks or actors) without causing data races.
// Value types of Sendable members are AUTOMATICALLY Sendable.
struct User: Sendable {
let id: Int
let name: String
}
// Most simple value types already conform implicitly.
let u = User(id: 1, name: "Ada")
// You can pass a Sendable value into a Task safely:
Task {
print("Sent across to a task: \(u.name)") // Ada
}2️⃣ @Sendable Closures
An @Sendable closure may only capture Sendable values, so it is safe to run concurrently. A Task's body is exactly such a closure.
// @Sendable closures may ONLY capture Sendable values, so they are
// safe to run concurrently. Task's body is an @Sendable closure.
func runConcurrently(_ work: @Sendable @escaping () -> Void) {
Task { work() }
}
let greeting = "Hello" // a Sendable String
runConcurrently {
// Capturing 'greeting' is fine — String is Sendable.
print(greeting) // Hello
}
// Capturing a mutable, non-Sendable reference here would be a
// compiler error under strict concurrency.3️⃣ Actor Isolation, @MainActor, nonisolated
An actor isolates mutable state, making it safely shareable. @MainActor pins code to the main thread; nonisolated opts a member out so it needs no await.
// Actors isolate mutable state; @MainActor pins code to the main actor.
actor BankAccount {
private var balance = 0 // isolated state
func deposit(_ amount: Int) { balance += amount }
func currentBalance() -> Int { balance }
// nonisolated opts OUT of isolation — no await needed to call it.
nonisolated let currency = "USD"
}
@MainActor
func updateLabel(_ text: String) { // runs on the main actor
print("UI: \(text)")
}
let account = BankAccount()
Task {
await account.deposit(100) // cross-actor -> await
let total = await account.currentBalance()
print("Currency \(account.currency), total \(total)") // no await on currency
}Your turn. Fill in the safety protocol and the closure attribute.
// 🎯 YOUR TURN — fill in each ___ then run it in Xcode (Swift 6 mode).
// 1) Mark this value type as safe to share across domains.
struct Point: ___ { // 👉 the protocol for cross-domain safety
let x: Int
let y: Int
}
// 2) This closure must only capture Sendable values.
func schedule(_ job: ___ @escaping () -> Void) { // 👉 the closure attribute
Task { job() }
}
let p = Point(x: 1, y: 2)
schedule { print("Point \(p.x), \(p.y)") } // Point 1, 2
// ✅ Expected output:
// Point 1, 2Pro Tips
- 💡 Prefer value types. Structs of Sendable members are Sendable for free.
- 💡 Wrap shared mutable state in an actor instead of using manual locks.
- 💡 Use @MainActor for UI code and nonisolated for immutable members.
- 💡 Adopt strict checking gradually via warnings before full Swift 6 mode.
Common Errors (and the fix)
- "Capture of 'x' with non-Sendable type": capture a value type or isolate it in an actor.
- "Non-sendable type ... crossing boundary": make the type Sendable or send a copy.
- UI updated off the main actor: mark the function @MainActor.
- Forcing it through: only use @unchecked Sendable when you truly guarantee safety.
📋 Quick Reference
| Concept | Code | Meaning |
|---|---|---|
| Safe to share | struct S: Sendable | no data races |
| Safe closure | @Sendable () -> Void | captures Sendable |
| Isolated state | actor A { … } | serialized access |
| Main thread | @MainActor | UI-safe |
| Opt out | nonisolated let x | no await |
| Manual promise | @unchecked Sendable | you guarantee it |
Mini-Challenge: Make It Race-Free
Wrap a shared counter in an actor so many tasks can update it safely.
// 🎯 MINI-CHALLENGE: make it race-free
// You have a counter shared by many tasks. Make it safe.
// 1. Define actor Counter with a private var value = 0.
// 2. Add func increment() and func current() -> Int.
// 3. From a Task, await two increments, then await current() and print it.
//
// ✅ Expected output:
// 2
// your code here
Practice quiz
What does the Sendable protocol indicate about a type?
- It is safe to share across concurrency domains
- It is a class
- It is faster
- It can be serialized to JSON
Answer: It is safe to share across concurrency domains. Sendable marks a type as safe to pass across actor or task boundaries without data races.
Which kind of type is automatically Sendable when its members are Sendable?
- A global variable
- A class with mutable state
- A value type like a struct of Sendable values
- Any closure
Answer: A value type like a struct of Sendable values. Value types such as structs and enums made of Sendable members get Sendable conformance for free.
What does marking a closure @Sendable promise?
- It never returns
- It captures only values safe to use across concurrency domains
- It is async
- It runs on the main thread
Answer: It captures only values safe to use across concurrency domains. An @Sendable closure may only capture Sendable values, so it is safe to run concurrently.
What problem do actors solve in relation to Sendable?
- They format strings
- They speed up JSON
- They replace structs
- They isolate mutable state so it is safe to share concurrently
Answer: They isolate mutable state so it is safe to share concurrently. An actor isolates its mutable state, providing a safe way to share reference-type state across tasks.
What does @MainActor guarantee about annotated code?
- It runs on the main actor, typically for UI work
- It is private
- It never suspends
- It runs on a background thread
Answer: It runs on the main actor, typically for UI work. @MainActor pins code to the main actor, which is where UI updates must happen.
What does the nonisolated keyword do on an actor member?
- Deletes it
- Opts a member out of the actor's isolation so it needs no await
- Makes it async
- Makes it isolated
Answer: Opts a member out of the actor's isolation so it needs no await. nonisolated removes a member from actor isolation, so callers can use it synchronously without await.
Under Swift 6 strict concurrency, data races are...
- Allowed at runtime
- Ignored
- Only warnings forever
- Caught by the compiler at build time
Answer: Caught by the compiler at build time. Swift 6 makes data-race safety a compile-time guarantee, turning many races into build errors.
Why might a class NOT be automatically Sendable?
- Classes are too fast
- Classes cannot conform to protocols
- It can have shared mutable reference state that risks races
- It has no methods
Answer: It can have shared mutable reference state that risks races. A class is a reference type with potentially shared mutable state, so it is not implicitly Sendable.
How can you assert a type is Sendable when you have verified its safety manually?
- @raw
- @unchecked Sendable
- @forceSendable
- @safe
Answer: @unchecked Sendable. Conforming with @unchecked Sendable tells the compiler you guarantee safety it cannot prove, such as with an internal lock.
Crossing from one isolation domain to another with a non-Sendable value causes...
- A compiler diagnostic under strict concurrency
- Nothing
- Faster code
- Automatic copying always
Answer: A compiler diagnostic under strict concurrency. Sending a non-Sendable value across isolation boundaries is flagged by the compiler to prevent races.
Continue this course
- Previous: Swift Macros
Frequently asked questions
What is the Sendable protocol?
Sendable is a marker protocol that tells the compiler a type is safe to share across concurrency domains, meaning it can be passed between different tasks or actors without risking a data race. It carries no methods or requirements of its own; conforming is a promise about thread-safety that the compiler verifies. Value types like structs and enums built entirely from Sendable members conform automatically, while classes generally do not because they can hold shared mutable state.
What does an @Sendable closure mean?
Marking a closure @Sendable restricts what it is allowed to capture to only Sendable values, which guarantees the closure can run safely on a different task or thread without sharing mutable state unsafely. Many concurrency APIs, including the body of a Task and Task.detached, expect @Sendable closures. If you try to capture a non-Sendable reference, such as a mutable class instance, the compiler flags it, steering you toward a safe design like passing a copy or isolating the state in an actor.
How do actors and Sendable work together?
Actors are reference types that isolate their mutable state so only one task touches it at a time, which makes them a safe way to share mutable, reference-type data across concurrency domains. Because of that isolation, actor types are themselves Sendable. When you call into an actor from outside, the access is asynchronous and requires await, which is how the compiler enforces serialized, race-free access. Actors and Sendable together give you safe sharing without manual locks.
What do @MainActor and nonisolated do?
@MainActor pins a function, property, or type to the main actor, guaranteeing its code runs on the main thread, which is required for UI updates in UIKit and SwiftUI. The nonisolated keyword does the opposite for a member of an actor: it opts that member out of the actor's isolation, so it can be called synchronously without await, which is appropriate for members that only touch immutable, already-Sendable data such as a constant. Together they let you place code in exactly the right isolation domain.
What is Swift 6 strict concurrency checking?
Swift 6 turns data-race safety into a compile-time guarantee. Under strict concurrency checking, the compiler analyzes where values cross isolation boundaries and reports an error when a non-Sendable value would be shared unsafely between tasks or actors, rather than letting the bug surface as a crash or corruption at runtime. You can adopt it gradually by enabling stricter warnings in Swift 5 mode before turning on full Swift 6 mode. The payoff is that whole classes of hard-to-reproduce concurrency bugs are caught before you ever run the app.