Skip to content

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:

  1. You have a non-Sendable class
  2. That class owns a reference to an actor
  3. You create a Task that implicitly captures self to access the actor property:
swift
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:

swift
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 than self 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:

swift
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:
    1. Only allow capturing Sendable values
    2. Can't mutate captured state
    3. Get stricter memory safety enforcement in Swift 6

Why Capture Lists Matter

Swift's closure capture behavior:

  • Implicit self capture: Task { await worker.doWork() } captures entire self
  • 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

  1. Minimize Closure Captures:

    swift
    // Instead of:
    Task { await self.someCall() }
    
    // Prefer:
    Task { [actorProperty] in await actorProperty.someCall() }
  2. Convert to Async Functions:

    swift
    // Before
    func fetch() {
        Task { ... }
    }
    
    // After
    func fetch() async { ... }
  3. Adopt Sendable for Custom Types:

    swift
    struct User: Sendable {
        let id: UUID
        let name: String
    }
  4. Avoid Global Actors Unless Necessary:

    swift
    // Only annotate when truly needed
    @MainActor func updateUI() { ... }

IMPORTANT

Unannotated classes are not Sendable by default. Either:

  1. Make them value types (struct/enum)
  2. Add @unchecked Sendable with manual safety validation
  3. Convert to actors for mutable state

Complete Solution Code

swift
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.