Sendable and Swift 6 data-race safety

Swift · Advanced · 15 min

Swift 6 mode turns data-race warnings into errors, so every project migrating to it hits these diagnostics.

In the Swift 6 language mode, the compiler proves your program free of data races: mutable state may only be touched from one isolation domain (an actor, the main actor, or a single task) at a time. Values crossing between domains must be safe to share, which the `Sendable` protocol expresses.

What is Sendable:

  • Value types whose stored properties are all Sendable — implicitly for non-public structs and enums.
  • Actors (they protect their own state).
  • final classes with only immutable (let) Sendable properties.
  • Closures marked @Sendable, which may only capture Sendable values and can't mutate captured vars.

A plain class with var properties is not Sendable. Passing it into Task.detached, an actor method, or capturing it in a @Sendable closure gives errors like "passing argument of non-sendable type ... risks causing data races".

Fixes, in order of preference:

  • Make the type a struct, or make the class final with let properties.
  • Isolate it: put it on @MainActor or turn it into an actor.
  • Rely on region-based isolation: the compiler accepts a non-Sendable value passed across if it can prove the sender never uses it again.
  • As a last resort, @unchecked Sendable when you protect state yourself with a lock such as Mutex from the Synchronization module.

Migrate module by module: enable strict concurrency checking (complete) in Swift 5 mode first, fix the warnings, then switch to swiftLanguageModes: [.v6].

Example

import Synchronization
struct Order: Sendable { let id: Int; var items: [String] }   // value type: Sendable

final class Settings: Sendable {          // immutable final class
    let theme: String
    init(theme: String) { self.theme = theme }
}

final class Counter: Sendable {           // state guarded by a Mutex
    private let value = Mutex(0)
    func increment() -> Int {
        value.withLock { v in v += 1; return v }
    }
}

class Draft { var text = "" }             // NOT Sendable

let counter = Counter()
await withTaskGroup(of: Void.self) { group in
    for _ in 0..<100 { group.addTask { _ = counter.increment() } }
}
print(counter.increment())               // 101

let draft = Draft()
// Task.detached { draft.text = "x" }    // Swift 6 error: non-Sendable 'draft' captured

Remember

  • Sendable = safe to share across isolation domains
  • Structs/enums of Sendable parts, actors and immutable final classes are Sendable
  • Prefer restructuring or isolation over @unchecked Sendable

Watch out for

  • Silencing every error with @unchecked Sendable or nonisolated(unsafe), which just hides the races.

Check yourself

  1. Which type is Sendable without extra work?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.swift.org/migration/documentation/migrationguide/