[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-swift-6-strict-concurrency-data-races":3},"\u003Cp>Friday, 4:47 PM. I set \u003Ccode>SWIFT_VERSION\u003C\u002Fcode> to 6.0, hit Cmd+B, and watched 214 errors roll in. My first instinct was to revert the one-line change and pretend I'd never opened that dialog box.\u003C\u002Fp>\n\n\u003Cp>I'm glad I didn't. Buried in that wall of red were three data races I'd been shipping for over a year. Ones that hadn't crashed yet. Ones that were going to.\u003C\u002Fp>\n\n\u003Cp>Here's what the Swift 6 migration actually looked like, and which errors were real bugs instead of pedantry.\u003C\u002Fp>\n\n\u003Ch2>The setup, and why you should migrate slowly\u003C\u002Fh2>\n\n\u003Cp>The app is a two-year-old SwiftUI app. Photo-heavy, with a background sync layer and a shared in-memory cache. It had been sitting in Swift 5 mode with strict concurrency set to \u003Ccode>minimal\u003C\u002Fcode>, which in practice means the compiler just doesn't look.\u003C\u002Fp>\n\n\u003Cp>\"Eventually\" was last week.\u003C\u002Fp>\n\n\u003Cp>The order that worked: \u003Ccode>SWIFT_STRICT_CONCURRENCY = targeted\u003C\u002Fcode> first, treating every warning as a task. Then \u003Ccode>complete\u003C\u002Fcode>. Only then flip the language mode, module by module. Flip the whole project at once and the error list stops being a checklist and becomes a eulogy.\u003C\u002Fp>\n\n\u003Ch2>Bug one: the cache that was fine by luck\u003C\u002Fh2>\n\n\u003Cp>First up, the cheapest one — my thumbnail cache:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">final class ThumbnailCache {\n    private var images: [URL: UIImage] = [:]\n\n    func image(for url: URL) -&gt; UIImage? { images[url] }\n    func store(_ image: UIImage, for url: URL) { images[url] = image }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Writes happen on background threads during sync; reads happen on the main thread while the user scrolls. It worked in production because my sync loop happened to serialize the writes.\u003C\u002Fp>\n\n\u003Cp>That's not a design. That's a coincidence wearing a design's clothes.\u003C\u002Fp>\n\n\u003Cp>The compiler called it: \u003Ccode>capture of 'cache' with non-Sendable type in @Sendable closure\u003C\u002Fcode>. The fix was one keyword:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">actor ThumbnailCache {\n    private var images: [URL: UIImage] = [:]\n\n    func image(for url: URL) -&gt; UIImage? { images[url] }\n    func store(_ image: UIImage, for url: URL) { images[url] = image }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Same body. Every call site gains an \u003Ccode>await\u003C\u002Fcode> and the race is gone — the actor serializes access the way my sync loop was doing by accident. Zero crashes since.\u003C\u002Fp>\n\n\u003Ch2>Bug two: the view model that wasn't on the main actor\u003C\u002Fh2>\n\n\u003Cp>This one is subtler, and I bet half of you have the exact same code:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">final class FeedViewModel {\n    @Published var posts: [Post] = []\n\n    func load() {\n        Task {\n            let fetched = await FeedService.shared.fetchPosts()\n            posts = fetched\n        }\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>No \u003Ccode>@MainActor\u003C\u002Fcode> anywhere. So that \u003Ccode>Task { }\u003C\u002Fcode> inherits nothing from the class — it runs on a background executor, and \u003Ccode>posts = fetched\u003C\u002Fcode> is a UI write on a thread it has no business being on. In Swift 5 with minimal checking this compiles and mostly behaves — the timing is what saves you.\u003C\u002Fp>\n\n\u003Cp>The compiler refused the gamble: \u003Ccode>main actor-isolated property cannot be referenced from a nonisolated context\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>The fix is to make the class own its isolation contract:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">@MainActor\nfinal class FeedViewModel {\n    @Published var posts: [Post] = []\n\n    func load() {\n        Task {\n            let fetched = await FeedService.shared.fetchPosts()\n            posts = fetched\n        }\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Now \u003Ccode>Task { }\u003C\u002Fcode> inherits main-actor isolation and the capture is legal. Rule of thumb: if a function touches \u003Ccode>@Published\u003C\u002Fcode> state, it belongs on the main actor. State the contract on the type, not across every call site.\u003C\u002Fp>\n\n\u003Ch2>Bug three: the singleton with mutable state\u003C\u002Fh2>\n\n\u003Cp>Then there was the classic:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">final class AppSettings {\n    static let shared = AppSettings()\n    var theme: Theme = .system\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Ccode>Static property 'shared' is not concurrency-safe\u003C\u002Fcode>. Everyone's favorite error. The lazy fix is \u003Ccode>@MainActor\u003C\u002Fcode> on the class — fine when settings are only touched by UI code. Mine were also written from a background sync callback, so an actor was the honest fix.\u003C\u002Fp>\n\n\u003Cp>Singletons are global variables with a better publicist. If they hold mutable state, they need an answer to \"who can touch this, and when?\" before Swift 6 asks it for you.\u003C\u002Fp>\n\n\u003Ch2>The @unchecked Sendable trap\u003C\u002Fh2>\n\n\u003Cp>With 214 errors, \u003Ccode>@unchecked Sendable\u003C\u002Fcode> looks like a gift from heaven. One annotation and the red disappears. But every suppression is a promise you make to the compiler that you know better than it does.\u003C\u002Fp>\n\n\u003Cp>I caught myself reaching for it about forty times. Each time I asked: can I explain in one sentence what actually protects this state? If yes, keep it and write that sentence as a comment. If no — and it was almost always no — that wasn't a fix, it was a crash scheduled for a future release.\u003C\u002Fp>\n\n\u003Ch2>Would I do it again\u003C\u002Fh2>\n\n\u003Cp>Yes, and the bugs being real is the reason. I went in expecting to fight a compiler being precious about thread-safety theory. I came out having fixed a cache, a view model, and a singleton — three genuine races sitting in my codebase for over a year.\u003C\u002Fp>\n\n\u003Cp>Swift 6.2 made this easier for new code — default main-actor isolation means fresh modules mostly just behave. It's the old code that needs the patience. Migrate module by module, fix data flow instead of suppressing warnings, and treat the error count as a bug report, not an insult.\u003C\u002Fp>\n\n\u003Cp>I saved the cache-actor pattern and the main-actor rule in \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa> so the next migration starts from the working version instead of a blank file. Searching for \"actor cache\" at 5 PM beats re-deriving from scratch.\u003C\u002Fp>\n",1787812702811]