iosNudge

What is Sendable, and why does Swift 6 care about it?

HardAsked oftenConcurrency 3 min read

TL;DR

Sendable is a protocol with no methods. It means: "values of this type are safe to share across tasks and actors". Value types made of Sendable parts, actors, and immutable final classes are Sendable. Mutable classes are not. In Swift 6 language mode, the compiler checks this strictly. Sending a non-Sendable value across actors is an error.

Why it exists

A data race happens when two threads touch the same memory and one writes. Classes are shared by reference, so they can cause this. Swift needs to know which types are safe to pass to another task.

final class Counter { var value = 0 }   // not Sendable

let counter = Counter()
Task.detached { counter.value += 1 }    // Swift 6 error: capture of non-Sendable 'counter'
counter.value += 1

What is Sendable automatically

TypeSendable?
Int, String, Bool, Data, URL, DateYes
Struct / enum where all stored values are SendableYes (inferred if not public)
Array, Dictionary, Optional of Sendable elementsYes
ActorsYes, always
final class with only let properties of Sendable typesYes, if you declare it
Class with var propertiesNo
ClosuresOnly if marked @Sendable (or inferred)

Structs

struct User: Sendable {
    let id: UUID
    var name: String
}

For internal types, Swift infers Sendable for you. For public types you must write it, because it becomes part of your API.

Final classes

final class Config: Sendable {
    let baseURL: URL
    let timeout: Double
    init(baseURL: URL, timeout: Double) {
        self.baseURL = baseURL
        self.timeout = timeout
    }
}

Must be final, and every stored property must be a let of a Sendable type.

@unchecked Sendable

For a class that protects its own state, for example with a lock, the compiler cannot prove it is safe. You promise it.

final class TokenStore: @unchecked Sendable {
    private let lock = NSLock()
    private var token: String?

    func set(_ new: String) { lock.withLock { token = new } }
    func get() -> String? { lock.withLock { token } }
}

If you are wrong, you get real data races. Use it carefully and add a comment.

Better: Mutex (iOS 18+)

The Synchronization module has Mutex, which is Sendable. No @unchecked needed.

import Synchronization

final class TokenStore: Sendable {
    private let token = Mutex<String?>(nil)

    func set(_ new: String) { token.withLock { $0 = new } }
    func get() -> String? { token.withLock { $0 } }
}

@Sendable closures

A closure that runs in another task must not capture mutable state unsafely.

func runLater(_ work: @escaping @Sendable () -> Void) {
    Task.detached { work() }
}

var count = 0
// runLater { count += 1 }   // error: mutation of captured var in @Sendable closure

let snapshot = count
runLater { print(snapshot) }  // OK: captures an immutable Sendable value

Rules for @Sendable closures:

  • Captured values must be Sendable.
  • Captured variables must not be mutable vars.

sending: transfer instead of share

Swift 6 can move a non-Sendable value to another actor if you never use it again. The sending keyword marks such parameters and results.

actor Library {
    var books: [Book] = []
    func add(_ book: sending Book) { books.append(book) }   // Book is a non-Sendable class
}

func addNewBook(to library: Library) async {
    let book = Book(title: "Swift")
    await library.add(book)      // OK: book is transferred
    // book.title = "x"          // error: book was sent away
}

How to fix "non-Sendable" errors

  1. Change the class to a struct, if it is just data.
  2. Make the class immutable and final.
  3. Turn it into an actor, if it has shared mutable state.
  4. Keep it on one actor, like @MainActor, so it never crosses.
  5. Protect it with Mutex or a lock, as a last step.

Common follow-up questions

  • Is Sendable checked at run time? No. It is a compile-time check only. There is no runtime cost.
  • Why are @MainActor classes Sendable? All their state is protected by the main actor. Any access from elsewhere needs await, so sharing the reference is safe.
  • What is @preconcurrency? It softens Sendable errors for types from modules that are not updated for Swift concurrency yet.
  • Can a protocol require Sendable? Yes: protocol Service: Sendable { }. All conforming types must then be Sendable.
  • Is [String: Any] Sendable? No, because Any might hold a class. Use a concrete type or [String: any Sendable].