Understanding Sendable in Swift
Learn what Sendable means in Swift concurrency and how it helps prevent data races.
06 Jul 2026 · 8 min read
When working with Swift concurrency, we often pass values between tasks, actors, and async functions.
We may fetch data in one task, send it to another part of the app, update the UI, or ask an actor to store something for us. The part to watch out for here is that concurrent code can run at the same time.
If two parts of the program can access and mutate the same value at once, we can end up with a data race. Sendable helps Swift check whether values are safe to pass across concurrency boundaries, such as into a task or actor.
In this article, we'll look at what Sendable means in more detail, when Swift checks it, how to make our own types conform to it, and what to do when the compiler complains.

What is Sendable?
Sendable is a protocol that marks a type as safe to pass across concurrency boundaries.
A concurrency boundary is a place where data moves from one isolated context to another. For example, when we call a method on an actor, Swift may need to move values between the caller's context and the actor's isolated context:
actor UserStore {private var users: [User] = []func save(_ user: User) {users.append(user)}}let store = UserStore()await store.save(user)
Here, user moves from the caller's isolation context into UserStore’s actor isolation. That is a concurrency boundary.
The compiler will check whether this value can be safely transferred. How strictly it does so depends on the Swift version and concurrency settings. In Swift 6 with strict concurrency enabled, Sendable violations are errors. In Swift 5 with the default settings, many of them are warnings.
For value types like structs and enums, this usually works without any extra code because Swift can infer sendability when all stored properties are sendable:
struct User {let id: UUIDlet name: String}
Like many standard Swift types, UUID and String are sendable, and User does not contain shared mutable reference state.
We can also make this promise explicit by conforming to Sendable:
struct User: Sendable {let id: UUIDlet name: String}
Adding Sendable documents the intent of the type and lets the compiler help us keep it safe over time. For example, if someone later adds a mutable reference-type property to User:
struct User: Sendable {let id: UUIDlet name: Stringvar session: Session // ❌ compiler error if Session is not Sendable}
If User itself becomes a mutable reference type, we also get a compiler error that says something like: Sending self.user risks causing data races. Let's look at how to handle reference types.
Sendable with classes
Classes need more care because they are reference types. When we pass a class instance around, we are passing a reference to the same object.
This can be safe if the class is immutable:
final class AppConfiguration: Sendable {let apiBaseURL: URLlet featureFlags: [String: Bool]init(apiBaseURL: URL, featureFlags: [String: Bool]) {self.apiBaseURL = apiBaseURLself.featureFlags = featureFlags}}
This class is final, all stored properties are immutable, and all stored properties are Sendable.
But a mutable class is different:
final class Session: Sendable {var token: String?}
This is not safe. Two tasks could hold the same Session instance and mutate token at the same time.
In cases like this, we could reach for one of these options:
- Make it a value type. If the type doesn't need reference semantics, changing it to a struct is often the simplest fix.
- Make the class immutable. If mutation isn't needed after initialization, we can use let properties on a final class to satisfy the compiler.
- Protect mutable state with an actor. When the type genuinely needs mutable state that is accessed from concurrent code, an actor is usually the cleanest solution.
- Protect mutable state manually and use @unchecked Sendable. When neither of the above options fits, for example when wrapping a C library with its own thread-safety guarantees, we can take responsibility for synchronization ourselves.
Using an actor for mutable shared state
Actors are often the cleanest solution when a type owns mutable state that needs to be accessed from concurrent code.
Since actors protect their isolated state, we can move that state into one:
actor SessionStore {private var token: String?func updateToken(_ token: String) {self.token = token}func currentToken() -> String? {token}}
Calls from outside the actor go through await, which gives Swift a safe place to coordinate access.
let sessionStore = SessionStore()Task {await sessionStore.updateToken("abc")let token = await sessionStore.currentToken()}
Actor instances themselves can be passed between concurrency domains because their mutable state is protected by actor isolation.
What is @Sendable?
Sendable applies to types. @Sendable applies to function types and closures.
A @Sendable closure is a closure that can safely be called from concurrent code. This means its captures must also be safe. We'll look at @Sendable in more detail in a separate article.
What about @unchecked Sendable?
Sometimes we know a type is safe, but the compiler cannot prove it.
This may happen with reference types that protect their state internally:
final class Counter: @unchecked Sendable {private let lock = NSLock()private var value = 0func increment() {lock.lock()defer { lock.unlock() }value += 1}func currentValue() -> Int {lock.lock()defer { lock.unlock() }return value}}
The compiler sees mutable state and cannot verify that the lock is used correctly. By writing @unchecked Sendable, we take responsibility for that guarantee ourselves.
@unchecked Sendable should not be used just to make a compiler warning disappear. It does not make the type thread-safe. If the synchronization is wrong, the compiler will trust us anyway.
Conclusion
Using Sendable can feel strict at first, especially when enabling stricter concurrency checking in an existing project. But the warnings often point to real design questions around ownership, mutation, and shared state.
For most types, conformance requires little or no extra work: structs with sendable properties get it for free, and actors handle mutable shared state cleanly. The cases that require more thought are usually worth the attention.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



