WCSession.sendMessage is crashing when a reply or error handler is attached, why?

The following code should send a message from the watch to the iPhone. Unfortunately, it is crashing. I have no clue why.

if WCSession.isSupported()
{
  let session = WCSession.default

  if ((session.activationState == .activated) && session.isReachable)
  {
    session.sendMessage(["SimpleMessages":["start"]],                                          replyHandler:{reply in
                                                     _ = 0
                                                    },
                            errorHandler:nil)
      }
    }

If the reply handler is set to nil, the code does not crash.

Anyway, in both cases the message is correctly sent from the watch to the phone.

The code itself is run on the main thread (verified).

I am running watchOS 26.6 and iOS 27.

Here is the crash :

Answered by DTS Engineer in 907961022

This is one of a class of problems related to the way that Swift concurrency treats methods with callback from Objective-C. Swift is expecting the callback to be delivered on the main actor. That’s not the defined semantics of the sendMessage(_:replyHandler:errorHandler:) method, and hence you end up trapping inside the Swift runtime.

My general advice on this front is to switch to the Swift async function variant of the method. However, that won’t work in this case; Swift doesn’t create an async function variant of this method because it’s not sure which callback is the correct completion handler.

Given that, your best option is to mark the callback closures as sendable:

session.sendMessage(
    …,
    replyHandler: { @Sendable reply in
        …
    },
    errorHandler: { @Sendable error in
        …
    }
)

This tells the compiler that the callbacks are safe to call from any context, and thus it won’t insert its main actor check.

IMPORTANT The real fix for this problem would be for Apple to annotate the Watch Connectivity framework headers so that the Swift compiler knows that these closures are not called on the main actor. As you work with Watch Connectivity you’ll find other cases where this is an issue. For example, the session delegate methods (WCSessionDelegate) are called from a background queue but Swift doesn’t know that. There are fixes for all of these issue, but it’s not a smooth path.

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

Accepted Answer

This is one of a class of problems related to the way that Swift concurrency treats methods with callback from Objective-C. Swift is expecting the callback to be delivered on the main actor. That’s not the defined semantics of the sendMessage(_:replyHandler:errorHandler:) method, and hence you end up trapping inside the Swift runtime.

My general advice on this front is to switch to the Swift async function variant of the method. However, that won’t work in this case; Swift doesn’t create an async function variant of this method because it’s not sure which callback is the correct completion handler.

Given that, your best option is to mark the callback closures as sendable:

session.sendMessage(
    …,
    replyHandler: { @Sendable reply in
        …
    },
    errorHandler: { @Sendable error in
        …
    }
)

This tells the compiler that the callbacks are safe to call from any context, and thus it won’t insert its main actor check.

IMPORTANT The real fix for this problem would be for Apple to annotate the Watch Connectivity framework headers so that the Swift compiler knows that these closures are not called on the main actor. As you work with Watch Connectivity you’ll find other cases where this is an issue. For example, the session delegate methods (WCSessionDelegate) are called from a background queue but Swift doesn’t know that. There are fixes for all of these issue, but it’s not a smooth path.

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

Hi,

thanks! It would also be nice if Apple documents mention somewhere that this does not work. Even worse: actually, the documentation states explicitly that the handlers are called on a background thread! How can you then expect that it does not work?

PS: Old sample code from Apple has the same structure. Therefore, - as I do not believe that it crashed when it was built - it seems to be that this issue has been introduced once between 2024 (creation of sample code) and today.

Best regards Hardy

It would also be nice if Apple documents

Well, it’d be better if we added the NS_SWIFT_SENDABLE macro to the header so that the Swift compiler could do the right thing. If you’d like to see that happen, I encourage you to file a bug along those lines. And if you do that, please post your bug number, just for the record.

As to why the old sample code works, that’s almost certainly because it’s compiling in the Swift 5 language mode. That mode will report warnings about Swift concurrency problems that it detects, but:

  • Those are warnings, not errors.
  • It doesn’t insert the runtime traps that detect cases like this.

If you switched your project to the Swift 5 language mode then these crashes go away. But that’s not the right solution to this problem because:

  • The goal of the Swift 6 language mode is to flush out assumptions like this, so that you can apply the correct fix.
  • It could potentially turn this nice deterministic crash into a hard-to-reproduce data race )-:

If you’re curious about the backstory to this, there are a couple of Swift Evolution proposals to look at:

  • SE-0423 Dynamic actor isolation enforcement from non-strict-concurrency contexts — This introduced these traps.
  • SE-0463 Import Objective-C completion handler parameters as @Sendable — This avoids this gotcha for Objective-C completion handlers but, as I noted above, Swift doesn’t consider these closures to be completion handlers.

I have links to both, and more, in my Swift Concurrency Proposal Index.

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