Xcode should be decoupled from Swift versions
Those of you who follow me on Mastodon might have noticed that I recently ran into a compiler bug that was introduced in Xcode 26.5 (or more specifically, Swift 6.3.2). I was unusually late this time: I normally have the betas running, but didn’t, and even the release had been out for a few days when I installed it. Like any good developer, the first thing I did was run our test suite.
To my surprise, some tests crashed on a MainActor assertion. I remember this kind of bug from the Objective-C days, sometimes as a regression after an update. But the project this was happening in uses Swift 6.3 with the v6 language mode and all upcoming features enabled. The hill Swift 6 is willing to die on is the guarantee that anything inside a @MainActor function is reliably executed on the main actor. But here, a @MainActor function was being executed on a background thread. Smelled like a compiler bug.
The trigger
About half an hour later, after a few detours suspecting SwiftTesting and other culprits, we had distilled the issue down to a minimal reproduction:
@MainActor func example() async {
await function()
MainActor.preconditionIsolated() // ← Crash 💥!
}
var function: nonisolated(nonsending) () async -> Void {
return { @concurrent in
print("Boom")
}
}
await example()
Just run it with the swift CLI without any additional settings, and it will … crash 💥! This is a compiler bug!
Side note: Don’t get confused by the nonisolated(nonsending) and @concurrent annotations, these are not needed. There are several ways to trigger the problem. For example, in the closure, you could use an explicit capture instead: { [value] in …. And instead of the explicit nonisolated(nonsending), you could enable the upcoming feature flag NonisolatedNonsendingByDefault, which happens to be part of “Approachable Concurrency”, which happens to be a default setting for new Xcode projects. So the variable could also look as harmless as this:
var function: () async -> Void {
let value = "Boom"
return { [value] in
print(value)
}
}
In real code, the closure would do something more useful and value wouldn’t be a static let, this is a minimal example. But it could easily appear in many codebases… like ours. It’s a really simple, really basic use of the language, and it produces a crash. So you’d naturally ask:
How could this slip?
And here begins my rant… erm… opinion piece. Because it seems like this bug might not actually have slipped. It’s listed as a known issue in the Xcode 26.5 release notes. 🙄
I’ve tried to reconstruct the timing of the issue and the release. Here’s what I found:
- Xcode 26.5 beta 3 was the first build with the bug, released on 27 April 2026
- The release candidate (identical to the final release build) came out on 4 May 2026
- The first public report of the issue was on 10 May 2026, a Sunday
- Same for the initial assessment, at 11:20 pm that same Sunday (poor engineers working Sunday nights)
- Xcode 26.5 was released on 11 May 2026
I have zero inside knowledge about this, so maybe the issue was actually found even earlier. But it seems safe to assume it was known before the release. So the question becomes:
Why did it ship?
I’m speculating here. One plausible explanation is that the issue wasn’t fully understood at the time the release decision was made. The release notes hint at that — they’re written in a very complicated way. I had to read them like 10 times to even grasp what they were about. It’s also possible this section was added only later, after more reports came in.
The thing is, major Xcode releases are usually bound to major OS releases. Xcode 26.2 shipped the same day as iOS/macOS 26.2, and so did 26.4 and 26.5. The only recent exception is Xcode 26.3, which was about a week later than the OSes, likely because there were no SDK changes that required a simultaneous release.
So Xcode 26.5 shipping that day wasn’t up for debate. OS releases set the pace and Xcode follows along. I doubt a bug in an Xcode version would ever be able to delay an OS release.
I also don’t know if there would even have been enough time between detection of the bug and the release the following day to make a new Xcode build with a fix or workaround. All Apple could have done would be to revert the bundled Swift version to 6.3.1. But I’d guess the pipeline for building Xcode (including App Store processing) is measured in days rather than hours. Too short of a notice.
It’s also worth emphasizing that the bug had only been out in the wild for a week before the RC, and just two weeks before the final release.
How to improve?
I’ve spent waaaay more time researching this thing than I wanted. Time to bring it to a close. In my opinion, two things made this issue as bad as it now is:
- Swift pre-release versions being tested only very briefly and by hardly any people (who installs every beta every week when there’s nothing exciting in it for them?)
- Tight coupling of Swift versions to Xcode versions
Maybe not a perfect solution, but a significant step forward would be to decouple Swift and Xcode versions.
There should be a project-wide build setting that lets you change which Swift toolchain to use. Just like SWIFT_VERSION selects the language version, a SWIFT_TOOLCHAIN setting could pick the toolchain version. It could work just like swiftly: 6.3 gives you the newest stable 6.3 release, 6.2.1 gives you exactly that version, and so on. Xcode would define a minimum compatible version and then download toolchains the same way it downloads simulators or packages. There’d still be a bundled version, but switching would become trivial.
While this is no guarantee against the bug, I believe it would have significantly reduced both the risk and the fallout:
- Apple would no longer need to rush Swift versions out the door to match Xcode releases. They’d only bundle the latest stable release and ideally, if practical, only after it’s been out for a while. In this instance, Xcode 26.5 would have shipped with Swift 6.3.1 instead of an unfinished 6.3.2.
- Apple would no longer need to release an Xcode update just to update the bundled Swift version (like 26.4.1 to ship Swift 6.3.1). Developers could update independently and much more rapidly.
- Developers could more easily go back to older toolchain versions if problems occur. Currently, that also requires downgrading Xcode and the SDK, which might be a concern if you depend on some new API. Instead of suggesting code workarounds in release notes, they could have suggested reverting to an older toolchain.
- It would be much easier to test upcoming Swift versions against production code. Adventurous developers could even use the
main-snapshottoolchain occasionally, providing much better feedback on upcoming versions. Again, no Xcode update needed.
All of this already works if you’re using Swift directly, outside Xcode, e.g., with swiftly. Time to bring the beast up to speed.