withTaskCancellationHandler operation ignores MainActor isolation inside global function

I have created a global function, passing the isolation as parameter with #isolation as default.

Inside this global function withTaskCancellationHandler is called, expecting it to run operation on the same actor, because it itself has a isolation param with #isolation as default value.

Now I run the global function from a MainActor isolation context, but the operation closure does not execute on the MainActor :exploding_head:.

The onCancel closure works as expected, the operation not.

My guess is that because the operation closure is async it hops off the MainActor to the global concurrent actor. Or it simply doesn't respect the global function's correct #isolation value.

Here is a relatively simple example with comments to understand what's going on (tested on Swift 6, Xcode 16.3) - run it inside an Xcode command line tool or Swift executable:

import Foundation

// isolation param necessary, otherwise first precondion would fail, too
func myGlobalMethod(isolation: isolated (any Actor)? = #isolation) async throws {
  MainActor.preconditionIsolated() // doesn't abort

  // if in a global function it aborts
  await withTaskCancellationHandler(
    operation: { MainActor.preconditionIsolated() }, // <-- this aborts --
    onCancel: { MainActor.preconditionIsolated() } // this doesn't abort
  )
}

@MainActor
class MyClass {
  func myMethod() async throws {
    try await myGlobalMethod()

    // this instead would work just fine (maybe the actor
//    await withTaskCancellationHandler(
//      operation: { MainActor.preconditionIsolated() },
//      onCancel: { }
//    )
  }
}


let task = Task.detached {
  try await MyClass().myMethod()
}

// this is to test of `onCancel` has the same isolation issues
let cancelTask = Task.detached {
  task.cancel() // canceled without isolation, so global concurrent actor
}

try await task.value
await cancelTask.value // makes sure cancellation gets executed

this is sort of the essence of the issue; the operation parameter needs to be marked as @MainActor if you want it to be isolated to the main actor. without a global actor annotation in the closure signature, or a capture of an isolated parameter, the closure will be nonisolated, and will run off of any actor. there is an open PR that may make this less confusing in the future.

a similar thread regarding this issue as it relates to parameter isolation can be found here.

1 Like