Nonisolated(nonsending) causes crash

Calling publishTest in the following program causes an EXC_BAD_INSTRUCTION crash in the attempt to run publishTest's closure. The program is compiled in Swift 6 mode with “nonisolated(nonsending) by Default” set to true and all other compiler options set to Xcode's default.

import Foundation
import Combine

public struct S {
  nonisolated(nonsending) public func f(counter: (Int) -> Void) async {
    let isolation = #isolation
    print("f \(isolation, default: "∅")")

    await withTaskGroup(of: Void.self) { group in
      // We just create the group for a while; we never actually launch any async child tasks in it.

      let isolation = #isolation
      print("withTaskGroup \(isolation, default: "∅")")
      for i in 1..<4 {
        print("Setting counter to \(i)")
        counter(i)
        print("Sleeping")
        try? await Task.sleep(nanoseconds: 200_000)
      }
    }
  }
}


@MainActor public final class PublishTest: ObservableObject {
  @Published public var value: Int = 0
  public var s: S = S()
  public var subscription: AnyCancellable?

  func publishTest() async {
    print("value = \(value)")
    subscription = $value.sink { i in
      print("Subscriber sees value = \(i)")
    }
    await s.f { @MainActor i in value = i }
    print("value = \(value)")
    subscription = nil
  }
}

The output is:

value = 0
Subscriber sees value = 0
f Swift.MainActor
withTaskGroup ∅
Setting counter to 1

followed by the crash. I find it curious that the withTaskGroup body seems unaware of running on MainActor, but it happily calls the MainActor-isolated counter closure, which is not sendable, so it presumably is not actually crossing an isolation domain boundary.

If I change the nonisolated(nonsending) to @MainActor, then the code produces the expected result:

value = 0
Subscriber sees value = 0
f Swift.MainActor
withTaskGroup Swift.MainActor
Setting counter to 1
Subscriber sees value = 1
Sleeping
Setting counter to 2
Subscriber sees value = 2
Sleeping
Setting counter to 3
Subscriber sees value = 3
Sleeping
value = 3

Am I missing something about how nonisolated(nonsending) is supposed to work? The code does not escape the safe subset of Swift 6, so I'd expect a compiler error or warning if there is unsafe code in it.

I'm running in Xcode 26.3, Intel, MacOS 15.7.4.

I believe the issue here is that the body closure of withTaskGroup does not currently inherit the isolation of the calling function – it is nonisolated (in the "classical" sense). At the moment, there is no way I'm aware of to take a local reference to an #isolation value and "force" a closure to share that isolation. However, with the more verbose existing construct to inherit caller isolation via use of an isolated parameter, you can do this – if you capture the isolated parameter in the closure, it will be isolated to it (assuming no other explicit isolation annotations).

I think this is intended to (hopefully) be fixed in an upcoming compiler version as these runtime functions are updated to use nonisolated(nonsending) in more places. This change looks like it might do it in this case: [Concurrency] adopt nonisolated nonsending in withTaskGroup... by ktoso · Pull Request #87644 · swiftlang/swift · GitHub.

1 Like

The thing I'm curious about: Is the body of the withTaskGroup in the same isolation domain as f?

  • If it is, then the code should work without crashing.
  • If it is not, then the code shouldn't compile because non-sendable counter would be crossing an isolation boundary.

Looks like the intent (for the future) of the PR is to pick the former answer?

Only under certain circumstances. Since body is nonisolated by default, the general isolation rules for closures apply. I think there may be a bug here, but I haven’t had enough time to investigate further yet.

You could use something like this for now:

func f(
    _ isolation: isolated (any Actor)? = #isolation, 
    op: () -> Void) async {
    await withTaskGroup(of: Void.self) { _ in
        _ = isolation
        op()
    }
}