How does region-based isolation work, and what does the sending keyword add on top of Sendable?

10.5.1 · L5 · Interview

Predict first

A non-Sendable object is captured by a Task and then used again on the next line. Does it compile?

final class Box { var n = 0 }
func use() async {
    let b = Box()
    Task { b.n += 1 }
    b.n += 1
}
  • Yes
  • No: sending a closure that captures b risks a data race

Predict first

An object is passed to a sending parameter and then used again. Does it compile?

final class Box { var n = 0 }
func take(_ b: sending Box) async {}
func use() async {
    let b = Box()
    await take(b)
    b.n += 1
}
  • Yes
  • No: b was sent away and cannot be used afterwards

Predict first

The same call without the later use of the object:

final class Box { var n = 0 }
func take(_ b: sending Box) async {}
func use() async {
    let b = Box()
    await take(b)
}
  • It compiles
  • It does not compile

Mental model. Handing over a sealed parcel is safe if you truly give it away and never look inside again. The compiler is the clerk who checks you really did not keep a copy.

Plain Sendable is a type property: “values of this type are safe to share”, and most classes are not. That was too strict: passing a freshly created object to another task is safe if nobody else keeps a reference, even though its type is not Sendable. Region-based isolation (SE-0414) lets the compiler track this per value. Values that are connected by references belong to the same region; a region is either disconnected (reachable from nowhere else) or tied to an isolation domain (an actor, the main actor, a task). Sending a value that is in a disconnected region to another domain transfers the whole region, and the compiler then rejects any later use of it (or of anything in its region) in the original code. The error reads “sending ‘b’ risks causing data races” when you use it after sending.

The keyword sending makes this part of a function’s signature. A sending parameter requires the caller to give up the value: it must be in a disconnected region and cannot be used afterwards, so the callee is free to move it to another domain. A sending result promises the returned value is disconnected, so the caller may send it elsewhere. Task { } and Task.detached take sending closures, which is why capturing a non-Sendable object in a task and using it again afterwards is an error, while capturing a fresh one you never touch again compiles. The practical rules: prefer value types and immutable data; for classes, either make them Sendable (final, immutable, or protected by an actor or lock), isolate them to an actor (including @MainActor), or hand them over once using sending. Use @unchecked Sendable only for types that are genuinely thread-safe, such as ones protected by a lock; it silences the compiler and moves the proof to you. Region analysis is part of the SIL diagnostics pass, so the errors appear when you build, not when you only type-check.

The task may run concurrently with the second increment, and both touch the same object, so the compiler rejects it.

A sending parameter takes ownership of the value’s whole region, so the caller may not use it again.

A freshly created object is in a disconnected region, so handing it over is safe when nothing else refers to it.

Common traps

  • Using an object after sending it. Once its region is transferred, the original code may not touch it.
  • Marking a class @unchecked Sendable to silence the compiler. The race is still there; you have only hidden it.

They might follow up with

  • When is a non-Sendable object safe to pass to a task? When it is in a disconnected region and you do not use it afterwards.
  • What does a sending result promise? That the returned value is not shared with anything else.

Related questions

  • 10.4.1 What does an actor guarantee, and what is actor reentrancy?
  • 10.5.2 How do you migrate an existing app to Swift 6 and strict concurrency without breaking it?
  • 1.10.2 What does Sendable mean for a Swift type you write?

How do you migrate an existing app to Swift 6 and strict concurrency… →