Fixing 'Capture of self with non-sendable type' in Swift 6
Problem Statement
In Swift 6, you may encounter the error Capture of 'self' with non-sendable type 'MyClass' in a @Sendable
closure when using concurrency features. This occurs in your code when:
- You have a non-Sendable class
- That class owns a reference to an actor
- You create a
Task
that implicitly capturesself
to access the actor property:
class MyClass {
let worker: MyActor = MyActor()
func swift6IsJustGreat() {
Task {
// ERROR: Captures entire `self` when accessing actor
let number: Int = await worker.doWork()
print(number)
}
}
}
The compiler prevents this because:
- Task closures are
@Sendable
by default in Swift 6 - Capturing
self
(a non-Sendable type) inside a@Sendable
closure violates Swift's concurrency safety rules - Non-Sendable types can't be safely shared across concurrency domains
Solution 1: Capture the Actor Directly
The most straightforward fix is to explicitly capture only the actor property instead of the entire self
:
func swift6IsJustGreat() {
Task { [worker] in // Capture only the `worker` actor
let number: Int = await worker.doWork()
print(number)
}
}
Why This Works
- Actors implicitly conform to
Sendable
, making them safe to capture - Capturing
worker
rather thanself
satisfies the compiler's safety requirements - This solution requires zero architectural changes to your existing code
Best Practice Tip: Always use explicit capture lists with Task
to narrowly define what values are captured.
Solution 2: Make Function Async
Refactor the function to be async
instead of wrapping work in a Task
:
func swift6IsJustGreat() async {
let number: Int = await worker.doWork()
print(number)
}
// Usage at call site
Task {
await myClassInstance.swift6IsJustGreat()
}
Why This Works
- Avoids creating capturing closures entirely
- Shift the async work to the caller's context
- Requires changes to your API signature
Good For: Code restructuring points where you can transition your method signature to async
Key Concepts Explained
Sendable and @Sendable Closures
Sendable
: A Swift protocol marking types safe to share across concurrency domains@Sendable
closures: Special closures that:- Only allow capturing
Sendable
values - Can't mutate captured state
- Get stricter memory safety enforcement in Swift 6
- Only allow capturing
Why Capture Lists Matter
Swift's closure capture behavior:
- Implicit
self
capture:Task { await worker.doWork() }
captures entireself
- Explicit capture:
Task { [worker] in ... }
captures onlyworker
When dealing with actors:
- ✅ Actors are
Sendable
by declaration - ❌ Non-actor/mutable classes are not implicitly
Sendable
Best Practices for Swift 6 Concurrency
Minimize Closure Captures:
swift// Instead of: Task { await self.someCall() } // Prefer: Task { [actorProperty] in await actorProperty.someCall() }
Convert to Async Functions:
swift// Before func fetch() { Task { ... } } // After func fetch() async { ... }
Adopt Sendable for Custom Types:
swiftstruct User: Sendable { let id: UUID let name: String }
Avoid Global Actors Unless Necessary:
swift// Only annotate when truly needed @MainActor func updateUI() { ... }
IMPORTANT
Unannotated classes are not Sendable by default. Either:
- Make them value types (struct/enum)
- Add
@unchecked Sendable
with manual safety validation - Convert to actors for mutable state
Complete Solution Code
actor MyActor {
func doWork() -> Int { return 42 }
}
class MyClass {
let worker: MyActor = MyActor()
// Solution 1: Explicit capture
func methodA() {
Task { [worker] in
print(await worker.doWork())
}
}
// Solution 2: Async method
func methodB() async {
print(await worker.doWork())
}
}
// Usage
let obj = MyClass()
obj.methodA()
Task { await obj.methodB() }
Swift 6 Migration
Enable Strict Concurrency Checking in build settings to identify similar issues:
- Set
SWIFT_STRICT_CONCURRENCY = complete
- Gradually fix warnings instead of batch-converting
By following these patterns, you maintain Swift 6's concurrency safety while efficiently working with actors and non-Sendable container types.