Logo for tanaschita.com

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.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

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: UUID
let 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: UUID
let 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: UUID
let name: String
var 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: URL
let featureFlags: [String: Bool]
init(apiBaseURL: URL, featureFlags: [String: Bool]) {
self.apiBaseURL = apiBaseURL
self.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 = 0
func 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.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

async/await

concurrency

swift

Async cleanup with defer in Swift

Learn how to perform asynchronous cleanup operations using Swift's async defer support.

14 Jun 2026 · 3 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum