Unexpected Behavior with SWIFT_STRICT_CONCURRENCY = complete in Xcode 26 (Swift 6)

Hello!
I’m testing Swift 6 (not Swift 5) on Xcode 26, and I don’t understand why the following code compiles without any errors, even though I’ve set the build setting
SWIFT_STRICT_CONCURRENCY = complete.

I was expecting a compiler error telling me that I’m accessing a variable outside the MainActor, which should not be allowed.
However, I get only a compiler warning but no error, and instead, the app crashes at runtime .

I have no crash with swift 5

Here’s the code:

class Test: ObservableObject {
    let publisher = Publisher()
    var bag = Set<AnyCancellable>()

    init() {
        publisher.value.sink {
            print($0)
        }.store(in: &bag)
    }
}

@MainActor
class Publisher: @unchecked Sendable {
    var value = PassthroughSubject<Int, Never>()

    init() {
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            self.value.send(10)
        }
    }
}

It should not crash

and when I have this code I have a warning with crash

Main actor-isolated property 'publisher' can not be referenced from a Sendable closure

@MainActor

class Publisher: @unchecked Sendable {

    var value = PassthroughSubject<Int, Never>()

    init() {

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {

            self.value.send(10)

        }

    }

}

but this code no warning and no crash

@MainActor

class Publisher: @unchecked Sendable {

    var value = PassthroughSubject<Int, Never>()

    init() {

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {

           DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.value.send(10)
          }

        }

    }

}

in both case we have
self.value.send(10)

in


  @preconcurrency public func asyncAfter(deadline: DispatchTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @Sendable @convention(block) () -> Void)


so we should have the same warning?

Good question.

But, you suggest that this code “should not crash”:

I might suggest that the issue is not that it should not crash, but perhaps that this warning should really be a hard error. (I assume that there is some solid technical reason why this is a runtime check and not a compile-time error.) But, in general, interacting with an actor-isolated property from a non-isolated context is simply unsafe.


You then ask why this does not generate the warning:

@MainActor
class ExamplePublisher: Sendable {
    let value = PassthroughSubject<Int, Never>()

    init() {
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.value.send(10)
            }
        }
    }
}

There is a subtle optimization where the compiler is able to successfully infer that the closure supplied to DispatchQueue.main.async[After] is isolated to the main actor. Interestingly, there are limits to this optimization, though, as illustrated if you interact with the main queue indirectly; in the following, the compiler is unable to infer main actor isolation:

This will not crash, but will produce this warning.

In this contrived example, we can vouch that it is on the main queue, we can tell the compiler it is safe to infer main actor isolation (and perform the associated runtime check in debug builds, at least):

@MainActor
class ExamplePublisher: Sendable {
    let value = PassthroughSubject<Int, Never>()

    init() {
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            let queue = DispatchQueue.main
            queue.asyncAfter(deadline: .now() + 1) {
                MainActor.assumeIsolated {
                    self.value.send(10)
                }
            }
        }
    }
}

Obviously, none of this MainActor.assumeIsolated {…} silliness is necessary if one uses the main queue directly (or, better, simply retires GCD entirely).

But your main observation still remains: It is unclear why the original example only produces a warning at compile-time, while crashing at runtime. I am sure this will be unsatisfying, but for me, the take-home message is that one is well advised to heed all of the concurrency-related warnings in Swift 6.

2 Likes

Hello
First of all, I wanted to thank you for your feedback and the time you took to respond.
Your answer really reassures me — I was afraid I had misunderstood something.

I agree with you that the warning should actually be an error.
However, if I set
SWIFT_STRICT_CONCURRENCY = minimum,
I still get the crash, and I’m worried that the error might not be properly reported in that case.

But overall, I completely agree with everything you said — I just wanted to make sure I wasn’t the only one noticing this kind of behavior and to better understand what’s going on.

The “Strict Concurrency Checking” build setting is unlikely the change the fact that you have a crash. That is a runtime error.

This build setting controls what compile-time warnings you see (and only really affects Swift 5 mode). In Swift 6 mode, it overrides this setting, giving strict concurrency checking regardless of this build setting (and many of the former warnings become hard errors, your example notwithstanding).

Regarding this setting, as it applies to Swift 6 mode, the docs say:

2 Likes

As an aside, this failure to generate a compile-time error is limited to GCD. One could use, for example, a detached task instead of a GCD background queue:

@MainActor
class ExamplePublisher: Sendable {
    let value = PassthroughSubject<Int, Never>()

    init() {
        Task.detached {
            self.value.send(10) // ❌ Main actor-isolated property 'value' cannot be accessed from outside of the actor; this is an error in the Swift 6 language mode
        }
    }
}

In Swift 5 mode with strict concurrency checking, you get the expected a compile-time warning. And when you use Swift 6 mode, this warning is promoted to an error, again, as expected.

The compiler’s failure to report this as an error in Swift 6 mode only happens when using the GCD API (which should generally be excised from Swift concurrency codebases, anyway, with some very isolated exceptions). This issue is avoided if one replaces the unnecessary GCD API in conjunction with Swift concurrency API.

1 Like

Hello Robert :slight_smile:

I think I’ve got it now.
At first, I was surprised that I could access to Main actor-isolated property from a sendable and escaping closure.

And yes, if I write something like this:

@MainActor
class Ok {
    var publisher = PassthroughSubject<Int, Never>()
    
    init() {
        test {
            self.publisher.send(10)
        }
    }
    
    func test(operation: @escaping @Sendable () -> Void) {
        
    }
}

I do get the expected error:

Main actor-isolated property 'publisher' cannot be referenced from a Sendable closure

and this it’s logical because we are supposed to have a closure that can be executed anywhere

I was confused when I saw the signature of closure from asyncAfter that was also sendable and I was able to access to a Main actor-isolated property

execute work: @escaping @Sendable @convention(block) () -> Void

I just noticed that the method is prefixed with @preconcurrency:

@preconcurrency public func asyncAfter(
    deadline: DispatchTime,
    qos: DispatchQoS = .unspecified,
    flags: DispatchWorkItemFlags = [],
    execute work: @escaping @Sendable @convention(block) () -> Void
)

That explains why I don’t get any error.
Indeed, if I declare my function like this:

@preconcurrency func test(operation: @escaping @Sendable () -> Void) {
    
}

it prevents the compilation error.

However, I do find this quite risky, because you can end up with crashes if a @Sendable closure is executed via GCD and that closure accesses something isolated to the MainActor.

But yes Apple prevent us with a warning when we have *preconcurrency
*
Main actor-isolated property 'publisher' can not be referenced from a Sendable closure

Conclusion: thank you warning :smiley: and for your time

Yep, it is a serious mistake to use @preconcurrency in our own code just to silence an error/warning. We invariably want to resolve the error/warning, not just silence it. While the errors/warnings can sometimes feel like they are just annoyances that we want to work around, but we should take these to heart and identify how to fix the problem, not just how to silence it.

The @preconcurrency is used by library authors to support pre-concurrency codebases. See SE-0337’s @preconcurrency attribute on nominal declarations. (And we use @preconcurrency import for frameworks that haven’t even done that.) But @preconcurrency has a side-effect of silencing very important warnings/errors and, potentially, offering unwarranted assurances of safety. It is almost never something that we would introduce in our own application code.

FWIW, the same applies to using @unchecked Sendable: It should never be used to just silence warnings/errors. It should only be used if you have some mutable state that you are manually synchronizing with some other technique (e.g., locks) about which the compiler is unable to reason. And this is exceedingly brittle, as it is all too easy to introduce problems that the compiler cannot identify. (I only mention this because you used it in your Publisher definition. I might also advise against using a name like Publisher as it is likely to only avoid confusion with the Combine protocol of the same name.)

thank you for your answer :slight_smile:

yes it was an error to use @unchecked Sendable but I got the same issue with only sendable :slight_smile:

Yes I agree with you I should not used @preconcurrency. It allow him to understand why I had a warning in my sample. Now it’s more logical to me :slight_smile:

1 Like