# Crash in Combine Related to Swift 6 Concurrency

**URL:** <https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178>\
**Category:** Using Swift\
**Tags:** concurrency, combine, actors, swift6\
**Created:** [October 5, 2024, 12:25pm UTC](https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178 "2024-10-05T12:25:55Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![aetherealtech](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/aetherealtech/32/36879_2.png) [@aetherealtech](https://forums.swift.org/u/aetherealtech)\
**Post date:** [October 5, 2024, 12:25pm UTC](https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178/1 "2024-10-05T12:25:55Z")

</div>

I was writing a test for a custom `Publisher`, and to simplify the asynchrony inherent in the test I marked the test function as `@MainActor` (that way closures can write back to reference captured local values without racing). The primary closure I needed to inherit the `@MainActor` context of the function was the `sink` callback on the publisher. I just wrote it as `sink { ... }` and could mutate the local value in the closure without a compiler error about mutation from a concurrent access.

But then I got confused. How can this possibly work? The closure is inheriting the `@MainActor` context, but `Combine` is pre-concurrency, publishers generally just call `receive(input:)` synchronously and from whatever thread a value happens to come in on. How is it going to ensure the closure gets called from the `MainActor`?

Well the answer is: by crashing LOL.

I wasn't sure if this was the fault of my own publisher, so I stripped down the test a bare minimum that uses a `PassthroughSubject`, and I was able to reproduce the crash. Here's the code for it:

```swift
import Combine
import XCTest

struct SendableWrapper<T>: @unchecked Sendable {
    let value: T
}

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class TestCombineCrash: XCTestCase {
    @MainActor
    func testCombineCrash() async throws {
        var state = 0
        
        let publisher = PassthroughSubject<Int, Never>()
        
        let subscription = publisher.sink { value in
            state += 1
        }
        
        Task.detached { [publisher = SendableWrapper(value: publisher)] in
            publisher.value.send(0)
        }
        
        try await Task.sleep(nanoseconds: 1_000_000_000_000)
    }
}

```

From testing I determined that it basically crashes if the actor promises of the code aren't actually satisfied at runtime. That means: if you mark the `sink` closure as `@Sendable`, it stops crashing. Of course that also (correctly) triggers the concurrent mutation compiler error. If you mark `sink` as `@Sendable @MainActor`, it crashes again. If you `send` the value from the main thread (omit the `Task.detached` wrapper), it works with the bare closure, since it happens to publish from the main thread.

Something else I found that is really fascinating to me, and this is why that `SendableWrapper` is necessary. With the bare closure, if I just try to call the `publisher` directly from inside `Task.detached`, I get this error:

```swift
Main actor-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race

```

But then if I mark the `sink` closure as `@Sendable`, the error goes away. So somehow the compiler _is_ trying to prevent this situation from occuring, and getting the crash requires smuggling _something_ through an `@unchecked Sendable` (here it's the publisher, in the test for my custom publisher, which is `Sendable`, inside the publisher's `Subscription` I was smuggling the upstream `Subscriber` across async contexts with the same trick, but my understanding is that a subscriber in `Combine` is supposed to be thread-safe so this is okay).

But how does this work!? What is the compiler using to determine that this error should occur _only_ if `sink` takes in a closure not marked as `@Sendable`? How does it know to look there for what the rules are about use of the `publisher`? The `publisher` isn't captured by the closure, it's just the receiver of the message the closure it passed to. I'm only barely starting to learn about `sending` and actor-isolated regions so I figure it has something to do with this but I don't see any use of `sending` added to Combine, so it's a total mystery to me.

Anyways, I'm not sure if the crash is an oversight or an intentional "serves you right for using `@unchecked Sendable`" behavior of Swift 6.

---

<div class="post-metadata">

**Author:** ![WAHa.06x36](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/waha.06x36/32/10286_2.png) [@WAHa.06x36](https://forums.swift.org/u/WAHa.06x36)\
**Post date:** [November 5, 2024, 3:32pm UTC](https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178/2 "2024-11-05T15:32:08Z")

</div>

I've run into the same problem, and you don't really need `@unchecked Sendable` to trigger it either, you just need old-fashioned APIs that don't understand Swift concurrency. For instance, I managed to make this minimal test case:

```swift
final class TestCrash: XCTestCase {
    let subject = CurrentValueSubject<Int, Never>(1)

    @MainActor
    func testCrash() async throws {
        let cancellable = subject.sink { value in
            print("\(value)")
        }

        performSelector(inBackground: #selector(triggerSubject), with: nil) // Crashes
        //perform(#selector(triggerSubject), with: nil) // Does not crash

        try await Task.sleep(nanoseconds: 1000000000)

        _ = cancellable
    }

    @objc func triggerSubject() {
        subject.send(2)
    }
}

```

I feel Swift is being a bit too over-eager to first apply @MainActor and then defend it here.

---

<div class="post-metadata">

**Author:** ![Stan](https://avatars.discourse-cdn.com/v4/letter/s/51bf81/32.png) [@Stan](https://forums.swift.org/u/Stan)\
**Post date:** [July 9, 2025, 2:29am UTC](https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178/3 "2025-07-09T02:29:27Z")

</div>

I run in the same issue. To borrow your test case

```swift

@globalActor
public struct TestGlobalActor {
    public actor ActorType { }
    public static let shared: ActorType = ActorType()
}

final class TestCrash: XCTestCase {
    let subject = CurrentValueSubject<Int, Never>(1)

    @TestGlobalActor
    func testCrash() async throws {
        let cancellable = subject.sink { value in
            print("\(value)")
        }

        performSelector(inBackground: #selector(triggerSubject), with: nil) // Crashes
        //perform(#selector(triggerSubject), with: nil) // Does not crash

        try await Task.sleep(nanoseconds: 1000000000)

        _ = cancellable
    }

    @objc func triggerSubject() {
        subject.send(2)
    }

```

As shown, this issue isn’t limited to `MainActor`. If a Combine signal is sent from a different actor context than where the subscription was created, it will crash.

Notably, this behavior only occurs in Swift 6; in Swift 5, it does not crash.

---

<div class="post-metadata">

**Author:** ![r3econ](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/r3econ/32/27583_2.png) [@r3econ](https://forums.swift.org/u/r3econ)\
**Post date:** [September 9, 2025, 2:18pm UTC](https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178/4 "2025-09-09T14:18:01Z")

</div>

Is there any known workaround for this issue?

---

<div class="post-metadata">

**Author:** ![x-sheep](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/x-sheep/32/40748_2.png) [@x-sheep](https://forums.swift.org/u/x-sheep)\
**Post date:** [September 9, 2025, 2:26pm UTC](https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178/5 "2025-09-09T14:26:43Z")

</div>

I think the reason it crashes is because the closure that handles incoming values inherits the isolation of the `testCrash` function by default, even though Combine makes no guarantee about which thread or actor it lands on.

The workaround for me is to annotate the `sink` closure with `@Sendable` to explicitly clear any inherited isolation.

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 9, 2025, 2:32pm UTC](https://forums.swift.org/t/crash-in-combine-related-to-swift-6-concurrency/75178/6 "2025-09-09T14:32:06Z")

</div>

Yes, any capture of closure parameters into Combine APIs can trigger Swift 6 mode's runtime queue assertions. For example:

```swift
publisher
  .filter { _ in true }
  .sink { _ in }

```

If this was called from a `@MainActor` context, the closures passed to `filter` and `sink` would have assertions added that they're called from the main queue, even though they don't access any `@MainActor` isolated state. So if the publisher has a value sent from a different queue, the app will crash. You can add an explicit `receive(on:)` before calling any closure parameters, add `@Sendable in` to the closures you capture, or ensure you only send values from the expected queue.

```swift
publisher
  .filter { @Sendable _ in true }
  .receive(on: DispatchQueue.main)
  .sink { _ in }

```
