What are @unchecked Sendable and nonisolated(unsafe) promising, and what does the compiler still check when you use them?

10.10.3 · L5 · Interview

Predict first

Does a lock-protected @unchecked Sendable class and a nonisolated(unsafe) global compile?

final class Cache: @unchecked Sendable {
    private let lock = NSLock()
    private var d: [String: Int] = [:]
    func get(_ k: String) -> Int? { lock.lock(); defer { lock.unlock() }; return d[k] }
    func set(_ k: String, _ v: Int) { lock.lock(); d[k] = v; lock.unlock() }
}
nonisolated(unsafe) var legacy = 0
let cache = Cache()
Task { legacy += 1; cache.set("a", 1) }
await Task.yield()
print("compiles")
  • Yes, it prints "compiles"
  • No: unchecked is not allowed in Swift 6
  • No: the global needs a lock

Predict first

A fresh object is captured by a detached task and used again by the creator. Does it compile?

final class Counter { var n = 0 }
func start() {
    let c = Counter()
    Task.detached { c.n += 1 }
    print(c.n)
}
start()
  • Compile error
  • It compiles

Predict first

And when the creator never touches it again?

final class Counter { var n = 0 }
func start() {
    let c = Counter()
    Task.detached { c.n += 1 }
}
start()
  • It compiles
  • Compile error

Mental model. A signed waiver: the safety inspector steps aside, but if someone is hurt, the signature on the waiver is yours.

@unchecked Sendable says “trust me: this type is safe to share, I protect its state myself”. The compiler stops checking the stored properties, so the whole thread-safety argument is now yours: every access to mutable state must go through the lock or queue, including future edits by other people. Cache in the check qualifies only because every read and write takes the same lock. nonisolated(unsafe) on a variable says the same for a global or a stored property: “I know it is shared and mutable and I will keep it safe”. It exists for migration (a legacy global you cannot restructure yet) and for state protected by something the compiler cannot see (a lock, a one-time initialisation, a framework guarantee).

What is still checked: the rest of the program, so crossing isolation with other non-Sendable values is still diagnosed, and region-based isolation still applies to values you transfer without capturing them again (the detached task that captures a fresh Counter compiles as long as you never use it afterwards, and is rejected the moment the creator uses it again). Treat both hatches as debt: wrap them so the lock is the only way in (private storage, small API), add a comment saying what protects the state and why, prefer Mutex so you do not need the hatch, and aim to delete each one during the Swift 6 migration (Q298).

Both hatches tell the compiler to trust you; it still checks everything else.

Using the object after sending it into another task could race, so region analysis rejects it.

The value was transferred, nothing else can reach it, so it is safe.

Common traps

  • Using @unchecked Sendable on a class with internal mutation and no lock. The error goes away and the data race stays.
  • Spreading nonisolated(unsafe) across many globals. Each one is an undocumented race waiting for the next caller.

They might follow up with

  • How do I find which hatches I used? Search for the attributes; some teams lint for them and require a comment.
  • Is there a safe way to hand a mutable object to another task? Yes: transfer it with sending so the creator can no longer touch it (Q297).

Related questions

  • 10.10.1 When is a type Sendable, and what does the compiler check when you declare it?
  • 10.10.2 How do you choose between an actor, a Mutex, an unfair lock, an atomic and a serial queue to protect shared…
  • 10.9.5 What does the Swift 6.2 default actor isolation setting change, and how does it affect a global variable or…
  • 10.5.1 How does region-based isolation work, and what does the sending keyword add on top of Sendable?
  • 10.5.2 How do you migrate an existing app to Swift 6 and strict concurrency without breaking it?

← How do you choose between an actor, a Mutex, an unfair lock, an…