swiftactorswift-concurrencyswift6xcode16

Closures in actors: Sending 'nonSendable' risks causing data races


Why is this not allowed in Swift 6 (Xcode 16 Beta 3)?

class NonSendable { }

actor MyActor {
    func foo() {
        let nonSendable = NonSendable()

        for _ in 1...3 {
            // ✅ Compiles fine
            bar(nonSendable)
        }
        
        (1...3).forEach { _ in
            // ❌ Sending 'nonSendable' risks causing data races
            // 'self'-isolated 'nonSendable' is captured by a actor-isolated
            // closure. actor-isolated uses in closure may race against later
            // nonisolated uses
            bar(nonSendable)
        }
    }
    
    func bar(_: NonSendable) { }
}

Solution

  • Swift 5.10 was overly conservative regarding passing non-Sendable types to different contexts. Specifically, we might create a non-Sendable instance, and pass it to some other context, but not use it outside of that new context. Swift 6 (specifically SE-0414) has improved this. As WWDC 2024 video What’s new in Swift says:

    To ensure safety, complete concurrency checking in Swift 5.10 banned passing all non-Sendable values across actor isolation boundaries. Swift 6 can recognize that it is safe to pass non-Sendable values, in scenarios where they can no longer be referenced from their original isolation boundary.

    So, as you noted, in Swift 6 (in Xcode 16 beta 3), you will get a warning with the following code:

    sendable warning

    In this case, though, it is the presence of the reference to nonSendable in the for-in loop that affects its isolation region. E.g., remove that reference and the error goes away:

    actor MyActor {
        func foo() {
            let nonSendable = NonSendable()
    
            // for _ in 1...3 {
            //     bar(nonSendable)
            // }
    
            (1...3).forEach { _ in
                // ✅ Compiles fine
                bar(nonSendable)
            }
        }
    
        func bar(_ object: NonSendable) { }
    }
    

    no sendable warning

    This Swift 6 behavior is an improvement over Swift 5.10. See SE-0414 – Region based Isolation for a lengthy discussion regarding what improvements Swift 6 provides and the limitations that are still imposed.


    For the sake of clarity, your original example (with both the for-in loop and the forEach closure) did not actually manifest a data race. But the question is whether the compiler can guarantee that the code is free from races: At this point it cannot.

    In terms of work-arounds, either avoid attempting to use the nonSendable instance from two different regions, or make the object Sendable.