Free Swiftsonic ticket up for grabs, enter in 10 seconds. Go to the giveaway →
Giveaway: Free Swiftsonic ticket giveaway.
Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Swift Concurrency Course

Sendable and @Sendable closures explained with code examples

Sendable is a protocol in Swift that indicates a type is safe to share across concurrency domains like actors, tasks, and threads. When a type conforms to Sendable, the compiler verifies at compile time that passing it around can’t introduce data races. Together with the @Sendable attribute for closures, it’s one of the core building blocks of Swift’s data-race safety.

Before diving into the topic of sendables, I encourage you to read up on my articles around async/await, actors, and actor isolation. These articles cover the basics of the new concurrency changes, which directly connect to the techniques explained in this article.

When should I use Sendable?

The Sendable protocol itself is surprisingly simple:

public protocol Sendable {}

It’s an empty protocol without any requirements. Its only job is to act as a marker: conforming tells the compiler to verify that your type is thread-safe to pass across isolation domains.

The Sendable protocol indicates whether the passed value’s public API is thread-safe for the compiler. A public API is safe to use across concurrency domains when there are no public mutators, an internal locking system is in place, or mutators implement copy-on-write, like with value types.

Many standard library types already support the Sendable protocol, removing the requirement to add conformance to many types. As a result of the standard library support, the compiler can implicitly create support for your custom types.

For example, integers support the protocol:

extension Int: Sendable {}

Once we create a value type struct with a single property of type int, we implicitly get support for the Sendable protocol:

// Implicitly conforms to Sendable
struct Article {
    var views: Int
}

At the same time, the following class example of the same article would not have implicit conformance:

// Does not implicitly conform to Sendable
class Article {
    var views: Int
}

The class does not conform because it is a reference type and therefore mutable from other concurrent domains. In other words, the class article is not thread-safe to pass around, and the compiler can’t implicitly mark it as Sendable.

Implicit conformance when using generics and enums

It’s good to understand that the compiler does not add implicit conformance to generic types if the generic type does not conform to Sendable.

// No implicit conformance to Sendable because Value does not conform to Sendable
struct Container<Value> {
    var child: Value
}

However, if we add a protocol requirement to our generic value, we will get implicit support:

// Container implicitly conforms to Sendable as all its public properties do so too.
struct Container<Value: Sendable> {
    var child: Value
}

The same counts for enums with associated values:

Implicit Sendable protocol conformance won't work if children do not conform to Sendable.
Implicit Sendable protocol conformance won’t work if children do not conform to Sendable.

You can see that we automatically get an error from the compiler:

Associated value ‘loggedIn(name:)’ of ‘Sendable’-conforming enum ‘State’ has non-sendable type ‘(name: NSAttributedString)’

We can solve the error by using a value type String instead, as it already conforms to Sendable:

enum State: Sendable {
    case loggedOut
    case loggedIn(name: String)
}

Throwing errors from thread-safe instances

The same rules apply to errors that want to conform to Sendable:

struct ArticleSavingError: Error {
    var author: NonFinalAuthor
}

extension ArticleSavingError: Sendable { }

As the author is non-final and not thread-safe (more about that later), we will run into the following error:

Stored property ‘author’ of ‘Sendable’-conforming struct ‘ArticleSavingError’ has non-sendable type ‘NonFinalAuthor’

You can solve the error by making sure all members of ArticleSavingError conform to Sendable.

FREE 5-day email course: The Swift Concurrency Playbook by Antoine van der Lee

FREE 5-Day Email Course: The Swift Concurrency Playbook

A FREE 5-day email course revealing the 5 biggest mistakes iOS developers make with with async/await that lead to App Store rejections And migration projects taking months instead of days (even if you've been writing Swift for years)

How to use the Sendable protocol

Implicit conformance takes away a lot of cases in which we need to add conformance to the Sendable protocol ourselves. However, there are cases in which the compiler does not add implicit conformance while we know that our type is thread-safe.

Common examples of types that are not implicitly sendable but can be marked as such are classes isolated to a global actor and immutable classes:

/// User is immutable and therefore thread-safe, so can conform to Sendable
final class User: Sendable {
    let name: String

    init(name: String) { self.name = name }
}

There’s a third way to make a class Sendable: isolating it to a global actor. A class annotated with @MainActor is implicitly Sendable, since the actor guarantees serialized access to its mutable state. This is why view models isolated to the main actor can be passed around freely, even when they contain mutable properties.

The restriction of conforming to Sendable in the same source file

Sendable protocol conformance must happen within the same source file to ensure that the compiler checks all visible members for thread safety.

For example, you could define the following type within a module like a Swift package:

public struct Article {
    internal var title: String
}

The article is public, while the title is internal and not visible outside the module. Therefore, the compiler can’t apply Sendable conformance outside of the source file as it has no visibility of the title property, even though the title is using a Sendable type String.

The same problem occurs when trying to conform an immutable non-final class to Sendable:

Non-final immutable classes can't conform to Sendable
Non-final immutable classes can’t conform to Sendable

Since the class is non-final, we can’t conform to Sendable as we’re unsure whether other classes will inherit from User with non-Sendable members. Therefore, we would run into the following error:

Non-final class ‘User’ cannot conform to `Sendable`; use `@unchecked Sendable`

As you can see, the compiler suggests using @unchecked Sendable. We can add this attribute to our user instance and get rid of the error:

class User: @unchecked Sendable {
    let name: String

    init(name: String) { self.name = name }
}

However, this does require us to ensure it’s always thread-safe whenever we inherit from User. As we add extra responsibility to ourselves and our colleagues, I would discourage using this attribute instead of using composition, final classes, or value types.

How to use @Sendable

Functions can be passed across concurrency domains and will therefore require sendable conformance too. However, functions can’t conform to protocols, so Swift introduced the @Sendable attribute. Examples of functions that you can pass around are global function declarations, closures, and accessors like getters and setters.

Part of the motivation of SE-302 is performing as little synchronization as possible:

we want the vast majority of code in such a system to be synchronization free

Using the @Sendable attribute, we will tell the compiler that he doesn’t need extra synchronization as all captured values in the closure are thread-safe. A typical example would be using closures from within Actor isolation:

actor ArticlesList {
    func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] {
        // ...
    }
}

In case you would use the closure with a non-sendable type, we would run into an error:

let listOfArticles = ArticlesList()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredArticles = await listOfArticles.filteredArticles { article in
 
    // Error: Reference to captured var 'searchKeyword' in concurrently-executing code
    guard let searchKeyword = searchKeyword else { return false }
    return article.title == searchKeyword.string
}

Of course, we can quickly solve this case by using a regular String instead, but it demonstrates how the compiler helps us to enforce thread safety.

How to use @unchecked Sendable

@unchecked Sendable tells the compiler to skip verification entirely: you take over the responsibility of guaranteeing thread safety. It’s a valuable escape hatch for legacy code with internal locking mechanisms, but it should be your last resort.

A typical example is a class that synchronizes access using a lock:

final class MutableUser: @unchecked Sendable {
    private let lock = NSLock()
    private var name: String = ""

    func updateName(_ name: String) {
        lock.lock()
        defer { lock.unlock() }
        self.name = name
    }
}

The compiler can’t verify this class is thread-safe, but we know it is due to the lock. The risk is real, though: add one property that doesn’t use the lock, and you’ve silently introduced a potential data race without any compiler support.

You might not need @unchecked anymore

Since the arrival of the Synchronization framework, you can often write the same class as a regular, checked Sendable conformance:

import Synchronization

final class MutableUser: Sendable {
    private let name = Mutex("")

    func updateName(_ name: String) {
        self.name.withLock { $0 = name }
    }
}

Mutex is Sendable itself, and the compiler can now verify all stored properties. In other words: full data-race safety, no unchecked promises. For new code, I recommend reaching for an actor or a Mutex before falling back to @unchecked Sendable.

Sending non-Sendable values across isolation domains

Not every type needs to conform to Sendable to cross an isolation boundary. Since Swift 6, the compiler performs region-based isolation analysis: if it can prove no one else accesses a value after you pass it along, the transfer is safe, even for non-Sendable types.

class Article {
    var title: String
    init(title: String) { self.title = title }
}

func check() {
    let article = Article(title: "Swift")

    Task {
        print(article.title) // OK: the compiler proves exclusive access
    }
}

However, as soon as you access the value after the transfer, the compiler steps in:

func check() {
    let article = Article(title: "Swift")

    Task {
        print(article.title)
    }

    print(article.title) // Error: 'article' was sent into the task
}

The sending keyword

You can make this ownership transfer explicit in your APIs using the sending keyword:

actor Logger {
    func log(article: Article) {
        print(article.title)
    }
}

func printTitle(article: sending Article) async {
    let logger = Logger()
    await logger.log(article: article)
}

The sending parameter tells callers they hand over ownership: after passing the article, they can no longer use it. This allows APIs to accept non-Sendable types safely, and it’s the reason you’ll see sending appear in more and more standard library APIs.

Strict concurrency checking and the Swift 6 language mode

Strict concurrency checking started as an opt-in build setting and became the default in the Swift 6 language mode. If your project still builds with the Swift 5 language mode, you can opt in gradually using the SWIFT_STRICT_CONCURRENCY build setting with its minimal, targeted, and complete levels. Once you move to the Swift 6 language mode, complete checking is always on, and data-race issues become errors instead of warnings. I’ve covered this process in detail in Swift 6: What’s New and How to Migrate.

Good to know: Swift 6.2’s approachable concurrency features reduce the number of Sendable-related diagnostics significantly, since more code stays within a single isolation domain by default.

Enabling Strict Concurrency in Xcode

Xcode allows you to enable strict concurrency checking through the SWIFT_STRICT_CONCURRENCY build setting:

Enable strict concurrency checking to fix sendable conformances and prepare your code for Swift 6.
Enable strict concurrency checking to fix sendable conformances and prepare your code for Swift 6.

This build setting controls the compiler enforcement level of Sendable and actor-isolation checking.

  • Minimal: The compiler will only diagnose instances explicitly marked with Sendable conformance and equals the behavior of Swift 5.5 and 5.6. There won’t be any warnings or errors.
  • Targeted: Enforces Sendable constraints and performs actor-isolation checking for all your code that adopted concurrency like async/await. The compiler will also check Instances that explicitly adopt Sendable. This mode tries to strike a balance between compatibility with existing code and catching potential data races.
  • Complete: Matches the intended Swift 6 semantics to check and eliminate data races. This mode checks everything the other two modes do as well but performs these checks for all code in your project.

The strict concurrency checking build setting helps Swift move forward to data-race safety. Each of the warnings triggered related to this build setting might indicate a potential data race in your code. Therefore, it’s essential to consider enabling strict concurrency checking to validate your code.

The number of warnings you’ll get depends on how often you’ve used concurrency in your project. For Stock Analyzer, I had about 17 warnings to solve:

Concurrency related warnings indicating potential data races.
Concurrency-related warnings indicating potential data races.

These warnings can be intimidating, but with the knowledge from this article, you should be able to get rid of most of them and prevent data races from taking place. However, some warnings are out of your control since an external module triggers them. In my case, I’ve had a warning related to SWHighlight that does not conform to Sendable, while Apple defined it in their SharedWithYou framework.

The compiler triggered several warnings related to this same issue:

  • Capture of ‘highlight’ with non-sendable type ‘SWHighlight?’ in a @Sendable closure
  • Stored property ‘highlight’ of ‘Sendable’-conforming struct ‘SharedSymbol’ has non-sendable type ‘SWHighlight?’

One way of solving this would be to add Sendable conformance ourselves:

extension SWHighlight: Sendable { }

However, you’ll run into the following error:

Conformance to ‘Sendable’ must occur in the same source file as class ‘SWHighlight’; use ‘@unchecked Sendable’ for retroactive conformance

Following the suggestion, you would change the code as follows:

extension SWHighlight: @unchecked Sendable { }

The @unchecked attribute tells the compiler to disable concurrency checking for SWHighlight instances. While this removes the warnings we’ve seen earlier, it doesn’t solve the potential race conditions. It would be best if you only used the @unchecked attribute for reference types that do their internal synchronization without actors. These types are thread-safe, but there’s no way for the compiler to verify them. You can mark them as unchecked, in which you tell the compiler to take care of data races yourself.

In the above example of the SharedWithYou framework, it’s better to wait for the library owners to add Sendable support. In this case, it would mean waiting for Apple to indicate Sendable conformance for SWHighlight instances. For those libraries, you can temporarily disable Sendable warnings by making use of the @preconcurrency attribute:

@preconcurrency import SharedWithYou

It’s important to understand that we didn’t solve the warnings but just disabled them. There’s still a possibility of data races occurring with code from these libraries. If you’re using instances from these frameworks, you need to consider whether instances are actually thread-safe. Once your used framework gets updated with Sendable conformance, you can remove the @preconcurrency attribute and fix potentially triggered warnings.

Continuing your journey into Swift Concurrency

The concurrency changes are more than just async-await and include many new features you can benefit from in your code. Now that you’ve learned about Sendable, it’s time to dive into other concurrency features:

Conclusion

Sendable and @Sendable are the compiler’s way of verifying that values crossing concurrency domains can’t introduce data races. In most cases, implicit conformance and region-based isolation do the work for you. When they don’t, you now know the order to try: value types, actors, a Mutex, and only then @unchecked Sendable.

Sendable issues are where most Swift 6 migrations get stuck. In my Swift Concurrency Course, I dedicate a whole module to Sendable, including the migration strategies I used in my own apps.

If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

Are you ready to

Turn your side projects into independence?

Learn my proven steps to transform your passion into profit.