Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution

I'm keeping most information in an actor and I would like to save also a closure in it that I get from

func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void)

Task.init{
                await GeoreferenceQueue.shared.setBackgroundCompletionHandler(completionHandler)
            }
}

where GeoreferenceQueue is and actor, while the caller is a class. yet I receive error:

Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure

and

Sending task-isolated 'completionHandler' to actor-isolated instance method 'setBackgroundCompletionHandler' risks causing data races between actor-isolated and task-isolated uses

Answered by DTS Engineer in 887130022

So, yeah, the compiler is right to complain about this. That completion handler is intended to be called from the main thread. By passing it to a newly created task you allow it to be called from other contexts, which is Not Good™.

There isn’t one true way of resolving this issue. The best option is gonna vary based on how you’ve set up your code. For example, the following is a very expedient option that kinda matches your existing design:

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    …
    
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        let runCompletionHandler = { @MainActor in
            completionHandler()
        }
        Task {
            await GeoreferenceQueue.shared.addCompletionHandler(runCompletionHandler)
        }
    }
}

actor GeoreferenceQueue {
    static let shared = GeoreferenceQueue()

    func addCompletionHandler(_ h: (() async -> Void)) {
        …
    }
}

It works by wrapping the completion handler sync function in an async function that’s also bound to the main actor. This function is sendable because, regardless of where it’s called from, it’ll bounce to the main actor before called completionHandler. Given that, it’s fine to pass the wrapper to the GeoreferenceQueue actor.

However, that’s just one example. If I were in your shoes I’d probably move the code that keeps track of these completion handlers to a main-actor-isolated type and then loosely couple that type to GeoreferenceQueue via some sort of notification.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

So, yeah, the compiler is right to complain about this. That completion handler is intended to be called from the main thread. By passing it to a newly created task you allow it to be called from other contexts, which is Not Good™.

There isn’t one true way of resolving this issue. The best option is gonna vary based on how you’ve set up your code. For example, the following is a very expedient option that kinda matches your existing design:

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    …
    
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        let runCompletionHandler = { @MainActor in
            completionHandler()
        }
        Task {
            await GeoreferenceQueue.shared.addCompletionHandler(runCompletionHandler)
        }
    }
}

actor GeoreferenceQueue {
    static let shared = GeoreferenceQueue()

    func addCompletionHandler(_ h: (() async -> Void)) {
        …
    }
}

It works by wrapping the completion handler sync function in an async function that’s also bound to the main actor. This function is sendable because, regardless of where it’s called from, it’ll bounce to the main actor before called completionHandler. Given that, it’s fine to pass the wrapper to the GeoreferenceQueue actor.

However, that’s just one example. If I were in your shoes I’d probably move the code that keeps track of these completion handlers to a main-actor-isolated type and then loosely couple that type to GeoreferenceQueue via some sort of notification.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Unfortunately using your code as of:

func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void) {
            let runCompletionHandler = { @MainActor in
                    completionHandler()
            }
            Task.init{
                await GeoreferenceQueue.shared.addCompletionHandler(runCompletionHandler)
            }
}

produces:

Sending 'completionHandler' risks causing data races

And @MainActor in your code is blue as it were recognized, in my code it remain black. And moreover in the actor at:

func addCompletionHandler(_ h: @escaping (() async -> Void)) {
        backgroundCompletionHandler=h
   }

I have:

/Users/fbartolom/Documents/cocoa applications/iPuja/Classes/GeoreferenceQueue.swift:78:36 Invalid conversion from 'async' function of type '() async -> Void' to synchronous function type '() -> Void'

On the assignment of h to the variable

In practice previously I had an error message, now two...

Judging from your other threads, perhaps you're overusing actors and completion handlers. I've only used one actor and that was on my first Swift 6 app. I've learned a lot since then.

Completion handlers are a different problem. They are largely a legacy of Apple's now-abandoned GCD architecture. The Swift concurrency approach is to use sequential, but asynchronous code instead. That new Swift approach is great. But so many Apple APIs are still based on blocks/closures. Easily the vast majority of all the "sending" problems I've encountered have been with blocks/closures. I would love to be able to write the kind of asynchronous Swift code I see in the WWDC demos. But it seems like it's never possible with Apple APIs.

So my suggestion is to re-think the actor and the completion handler. Obviously, if you have an API that requires a completion handler, you often can't get around that. Sometimes APIs do have an asynchronous version. But if they don't, then you have to handle that code more carefully.

And when I look at the delegate method in question, I see that it actually does have an async version. I think your best solution is to just use that. Pass the identifier to your GeoreferenceQueue and let it handle it however it should.

I think this case, an actor might be appropriate. I was thinking about your Timer actor from the other thread. It's better to integrate timer functionality into the class where it's needed, avoiding completion handlers whenever possible.

Unfortunately using your code … produces … Sending 'completionHandler' risks causing data races

Hmmm, it didn’t when I tested it. Lemme try that again:

  1. Using Xcode 26.4, I created a new project from the iOS > App template template.
  2. As part of that, I selected Storyboard from the Interface popup.
  3. I changed the Swift Language Version build setting to Swift 6.
  4. I added GeoreferenceQueue.swift and copied my GeoreferenceQueue to it.
  5. I added my application(_:handleEventsForBackgroundURLSession:completionHandler:) method to the AppDelegate class.

It built without error.

But keep in mind that my example was meant to be an expedient option. I agree with Etresoft’s comments [1] that you should step back and consider whether you want to change your overall approach.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Well, the comments here at least (-: We’re still discussing the wider issue in one of your other threads.

Also I have no Swift 6.4 version in my Swift options. As you may see in the picture, both in the official Xcode version as the beta one.

I fixed the error in the actor by using this code:

private var backgroundCompletionHandler: (() async -> Void)?

and

func getBackgroundCompletionHandler() -> ((() async -> Void)?) {
        return backgroundCompletionHandler
    }
    
    func setBackgroundCompletionHandler(_ h: (() -> Void)?){
        backgroundCompletionHandler=h
}
    
 func executeCompletionHandler() async{
        await backgroundCompletionHandler?()
}

Yet my error on the app delegate at:

func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void) {
            let runCompletionHandler = {@MainActor in
                    completionHandler()
            }
            Task.init{
                await GeoreferenceQueue.shared.setBackgroundCompletionHandler(runCompletionHandler)
            }
    }

Converting function value of type '@MainActor @Sendable () -> Void' to '() -> Void' loses global actor 'MainActor'

is still there.

Now I set all functions in the actor to MainActor as well the variable storing the completion:

@MainActor private var backgroundCompletionHandler: (()  async -> Void)?
@MainActor func getBackgroundCompletionHandler() -> ((() async -> Void)?) {
        return backgroundCompletionHandler
    }
    
    @MainActor func setBackgroundCompletionHandler(_ h: (() async -> Void)?){
        backgroundCompletionHandler=h
    }
    
    @MainActor func executeCompletionHandler() async{
        await backgroundCompletionHandler?()
    }

and the error returned to the completionHandler() line of:

func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void) {
            let runCompletionHandler = {@MainActor in
                    completionHandler()
            }
            Task.init{
                await GeoreferenceQueue.shared.setBackgroundCompletionHandler(runCompletionHandler)
            }
    }

with:

Task-isolated 'completionHandler' is captured by a main actor-isolated closure. main actor-isolated uses in closure may race against later nonisolated uses

I found a solution by making the closure sendable:

func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping @Sendable () -> Void) {
            let runCompletionHandler = {@MainActor in
                    completionHandler()
            }
            Task.init{
                await GeoreferenceQueue.shared.setBackgroundCompletionHandler(runCompletionHandler)
            }
    }

Now all errors seem to have vanished.

Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution
 
 
Q