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
Taskthat implicitly capturesselfto 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
@Sendableby default in Swift 6 - Capturing
self(a non-Sendable type) inside a@Sendableclosure 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
workerrather thanselfsatisfies 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@Sendableclosures: Special closures that:- Only allow capturing
Sendablevalues - 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
selfcapture:Task { await worker.doWork() }captures entireself - Explicit capture:
Task { [worker] in ... }captures onlyworker
When dealing with actors:
- ✅ Actors are
Sendableby 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 Sendablewith 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.