Question

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) { }
}
 3  139  3
1 Jan 1970

Solution

 3

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.

2024-07-18
Rob