kostia.dev
Swift
Concurrency
iOS

@Sendable, sending and unchecked sendable. Region-Based Isolation

Jun 12, 2025
7 min read

A guide to region-based isolation, @Sendable, sending and unchecked sendable, and closure safety in Swift 6 with examples.

Changes in Task, @Sendable, and Region-Based Isolation in Swift 6


🔍 Error in Swift 5

In Swift 5, you may have tried code like this:

swift
func runTheExample() async throws { let user = User() Task { try await sendAnalyticsEvents(for: user) } }

Looks harmless, right? But in Swift 5, the compiler would complain:

Capture of 'user' with non-sendable type 'User' in a @Sendable closure

Why?

Because Task in Swift 5 took an @Sendable closure by default. If you captured a non-Sendable type (like most class instances), you'd hit a wall. You had to explicitly mark types as Sendable, or refactor to avoid the capture.

swift
var closure: @Sendable () async throws -> Void = { print(user.name) // ❌ Compilation error }

✅ What Changed in Swift 6

Swift 6 introduces region-based isolation. This change allows Task to safely capture non-Sendable values as long as the compiler can verify they don’t escape the closure.

Now the same code compiles:

swift
func runTheExample() async throws { let user = User() Task { try await sendAnalyticsEvents(for: user) // ✅ Works in Swift 6! } }

Swift understands that user only lives within the closure's lifetime. It doesn't leak across concurrency boundaries.

This is part of a broader movement in Swift Concurrency toward pragmatic safety.


📦 Sendable objects in Swift

  • It's safe to use concurrently
  • It can cross isolation boundaries
  • The compiler enforces these guarantees

✅ ✅ Actors are always Sendable

You can safely send actors between concurrency regions.

✅ Structs are Sendable if:

  • All their members are Sendable
  • They explicitly conform to Sendable (for public types)

🧱 Examples of Sendable Types

✅ A Sendable Struct

swift
struct SendableStruct { let id = UUID() var count = 0 }

❌ A Non-Sendable Struct

swift
class User {} struct NonSendableStruct { let id = UUID() let user = User() // User is a class and not Sendable }

✅ A Sendable Enum

swift
enum SendableEnum: Sendable { case one, two, three case user(SendableStruct) }

❌ A Non-Sendable Enum

swift
enum NonSendableEnum: Sendable { case one, two, three case user(User) // User is not Sendable }

An enum is Sendable if:

  • It has no associated values
  • Or all associated values are Sendable
  • And it conforms to Sendable

✅ A Sendable Class

swift
final class SendableClass: Sendable { let id = UUID() let count = 0 }

❌ A Non-Sendable Class

swift
final class NonSendableClass: Sendable { let id = UUID() let user = User() // User is not Sendable }

A class is Sendable if:

  • All members are Sendable
  • It has no mutable state
  • It's a final class
  • It conforms to Sendable

⚠️ Using @unchecked Sendable

There are cases where your type logically should be Sendable, but the compiler can’t verify it. In such cases, you can mark it as @unchecked Sendable manually.

It makes sense to use @unchecked if:

  • You know that your class is thread-safe (and have tests to prove it)
  • You intend to revisit and adjust your class at a later time
  • You use it only temporarily

This is useful during migrations and when working with legacy code, but be cautious — the compiler won’t protect you here.


🧵 Understanding @Sendable Closures

An @Sendable closure:

  • Should be safe to run from any isolation context, concurrently
  • Can only capture Sendable state

Defining and Using

swift
var myClosure: @Sendable () async throws -> Void = { // ... } func runCode(_ closure: @Sendable () -> Void) { // ... }

But here's the catch:

An @Sendable closure is probably not what you want if you're capturing non-Sendable types inside it.

Example:

swift
func passUserExample() async throws { let user = User() runSendableExample { print(user.name) // ❌ if closure is @Sendable } } func runSendableExample(_ closure: @Sendable () -> Void) { closure() }

This is technically safe, but won't compile unless you use Swift 6's sending modifier:

swift
func runSendableExample(_ closure: sending () -> Void) { closure() }

✅ In Swift 6, sending allows capturing non-Sendable types safely under compiler-verified constraints.


📬 Swift 6 Introduces sending

In Swift 6, closures can now be marked with sending, enabling safe capture of non-Sendable values when the compiler determines it's safe.

swift
func runSendableExample(_ closure: sending () -> Void) { closure() }

This allows:

swift
func passUserExample() async throws { let user = User() runSendableExample { print(user.name) // ✅ Compiles in Swift 6 } }

🧠 What's different?

  • @Sendable closures: strict and isolated — only Sendable values allowed
  • sending closures: compiler-analyzed and region-aware

🔒 Sending and Safety

Consider this:

swift
func passUserExample() async throws { let user = User() // First access: sending closure runSendableExample { print(user.name) } // Second access after sending print(user.name) } func runSendableExample(_ closure: sending @escaping () -> Void) { closure() }

This can race, because after user is "sent" to the closure, accessing it again concurrently can lead to issues.

⚠️ Rule of thumb: avoid using values after sending them.


⚠️ Summary: Swift 5 vs Swift 6

FeatureSwift 5Swift 6
Task closureAlways @SendableStill @Sendable, but supports region-based analysis
Non-Sendable captureCompiler errorAllowed if confined to region
Actor isolationManual coordinationBuilt-in via compiler
Type checksStrictContext-sensitive
Closure isolationRigidFlexible with sending
Swift 6 is more developer-friendly, without compromising on safety.

Related Posts

Swift
Concurrency
iOS
A deep dive into Swift's concurrency model, focusing on actors and region-based isolation.
6/10/2025
9 min read
3 shared tags
iOS
Swift
Performance
Swift implements four types of method dispatch: inlining, static, table, and message. Understanding which one applies and when explains some of Swift's most confusing behaviour — and helps you write faster code.
2/21/2026
10 min read
2 shared tags
Swift
Concurrency
Swift 6 Migration
Refactoring
Sendable
Region-Based Isolation
Practical refactoring tips for migrating to Swift 6 concurrency: @Sendable, sending, region-based isolation, and common error fixes.
6/13/2025
5 min read
2 shared tags