To avoid a Task capturing non-sendable self, one technique that can work is to capture just a sendable property of self in a task closure. In solving a slightly different problem, I was surprised to see that capturing a global actor isolated instance method, as in the following code, gives no error or warning.
class Bar {
@MainActor func mainActorMethod() {}
}
class Foo {
let bar = Bar()
init() {
self.start()
}
func start() {
Task { @MainActor [callBar] in
callBar()
}
}
@MainActor func callBar() {
self.bar.mainActorMethod()
}
}
Foo()
I have tried in both Xcode 16.2 and Xcode 26 beta 2 playgrounds. It seems to me that there could be a data race because the main actor isolated callBar method could access non-isolated state in Foo.
Also, as I was simplifying the code for this question, I observed this code might an edge case. If the Task is moved into Foo's init and does the same capture or captures bar.mainActorMethod, then there are errors. Xcode 26 beta2 understands the pattern better because in Xcode 16.2 the error message says: Pattern that the region based isolation checker does not understand how to check. Please file a bug.
open class C {
var s = ""
func f() {
// your version; this should produce an error for sending `self` but does not
Task { @MainActor [m] in m() }
}
func g() {
// "control" version, should be equivalent to the above, but does
// correctly error: `sending 'self' risks causing data races`
Task { @MainActor [self] in self.m() }
}
func h() {
// same as yours but explicitly referring to self after creating the closure
// makes the compiler realize the error of its ways: `sending value of
// non-Sendable type '@callee_guaranteed () -> ()' risks causing data races`
Task { @MainActor [m] in m() }
s = "hi"
}
@MainActor func m() {}
}
yes, this seems like a bug (please report it if you have a chance). FWIW i am hopeful that this outstanding PR may resolve the problem. compiling the original example locally with those changes applied yields the following diagnostic:
error: non-Sendable '@MainActor () -> ()'-typed result can not be returned from main actor-isolated function to nonisolated context
156 |
157 | func start() {
158 | Task { @MainActor [callBar] in
| |- error: non-Sendable '@MainActor () -> ()'-typed result can not be returned from main actor-isolated function to nonisolated context
| `- note: a function type must be marked '@Sendable' to conform to 'Sendable'
159 | callBar()
160 | }
Just following up here, we do emit appropriate diagnostics with ToT:
test.swift:13:24: error: sending 'self' risks causing data races [#SendingRisksDataRace]
11 |
12 | func start() {
13 | Task { @MainActor [callBar] in
| |- error: sending 'self' risks causing data races [#SendingRisksDataRace]
| `- note: task-isolated 'self' is captured by a main actor-isolated closure. main actor-isolated uses in closure may race against later nonisolated uses
14 | callBar()
15 | }
test.swift:13:24: error: non-Sendable '@MainActor () -> ()'-typed result can not be returned from main actor-isolated function to nonisolated context
11 |
12 | func start() {
13 | Task { @MainActor [callBar] in
| |- error: non-Sendable '@MainActor () -> ()'-typed result can not be returned from main actor-isolated function to nonisolated context
| `- note: a function type must be marked '@Sendable' to conform to 'Sendable'
14 | callBar()
15 | }
The error is being emitted since we are capturing self in a MainActor isolated double curry thunk. The return error is b/c we are returning the MainActor isolated thunk to the nonisolated caller before we pass it off to Task as a capture.
Really what we want here is the ability to recognize that we are emitting essentially a closure that we are returning (today the codegen just looks like a normal function), so we can emit a nice specific error message about the capture of self in the curry thunk so we can access callBar (or something like that).