swiftswift-concurrencyswift-compiler

Why does Swift Compiler say, Non-sendable type 'X' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary?


I'm curious, can someone help me deal with a swift 6 error? Consider this code:

class Foo {
    init() async throws {}
}

struct ContentView: View {
    var body: some View {
        Button ("Test") {
            Task {
                let _ = try await Foo()
            }
        }
    }
}

If the project's Swift Language Version is set to "Swift 6" (Xcode 16.0), then the let _ = ... line gives the following error:

Non-sendable type 'Foo' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary.

What is going on here? I already know how to make Foo Sendable - that's not what I'm looking for. Instead I'm trying to understand the situation regarding actors.

I believe that the Task closure will be run on the @MainActor. Somehow, the asynchronous initializer seems to be causing my Foo object to cross an actor boundary, but I don't see why. It probably has something to do with the implicit call referenced in the error. But what, exactly, is this implicit call? Could someone point me to some docs that explain this situation?

By the way, I know that making the Task detached solves this problem. But I still don't understand what's going on, exactly.


Solution

  • You said:

    I believe that the Task closure will be run on the @MainActor.

    Yes, the Task is isolated to the main actor. But Foo’s initializer is not.

    Foo.init is an asynchronous initializer that is not isolated to any particular actor, so it will run on a “generic executor”. Add a MainActor.preconditionIsolated() in Foo’s initializer, and you will see it is not on the main actor:

    class Foo {
        init() async {
            // if uncommented, the following results in runtime failure:
            //
            // MainActor.preconditionIsolated()
        }
    }
    
    struct ContentView: View {
        var body: some View {
            Button ("Test") {
                Task {
                    let _ = await Foo()
                }
            }
        }
    }
    

    That is the problem: Foo.init is non-isolated and is being returned to an isolated context; you cannot cross actor boundaries if Foo is not Sendable.