← All shorts
56Swift 5.7+2 min read

Sendable and Why the Compiler Shouts at You

Sendable marks types that are safe to pass across concurrency domains, and the compiler enforces it at every boundary.

Passing a reference type across a Task boundary used to compile without complaint even when two threads could mutate the same instance at once. Sendable turns that into a compile-time check instead of a runtime crash you find out about later.

#The old way

final class RequestLogger {
    var entries: [String] = []

    func log(_ message: String) {
        entries.append(message)
    }
}

let logger = RequestLogger()

Task {
    logger.log("request started")
}

Task {
    logger.log("request finished")
}

#The new way

actor RequestLogger {
    private(set) var entries: [String] = []

    func log(_ message: String) {
        entries.append(message)
    }
}

let logger = RequestLogger()

Task {
    await logger.log("request started")
}

Task {
    await logger.log("request finished")
}

struct RequestMetadata: Sendable {
    let path: String
    let statusCode: Int
}

func record(_ metadata: RequestMetadata, using logger: RequestLogger) async {
    await logger.log("\(metadata.path) -> \(metadata.statusCode)")
}

#Why it matters

  • The compiler rejects RequestLogger (the plain class) at any point it crosses into a Task or actor boundary, catching the race before it ships
  • Value types with only Sendable stored properties conform for free — most structs and enums need no extra work
  • Turning RequestLogger into an actor makes it Sendable automatically because the actor itself serializes access
  • Under the Swift 6 language mode, these checks become errors instead of warnings, closing the gap between "compiles" and "safe"

#Gotcha

A class can be forced to conform with @unchecked Sendable, which tells the compiler to trust you instead of verifying anything. That's sometimes legitimate for a type that's internally synchronized in a way the compiler can't see (a lock around every mutable property, for instance), but reaching for it just to silence a warning without actually auditing the type's thread-safety reintroduces the exact race the annotation exists to prevent.

swiftconcurrency

Related shorts