Member-only story
Major Concurrency Changes in Swift 6.1 — What’s New and Why It Matters 🚀
Safer Async, Smarter Threads: How Swift 6.1 is Changing the Game for Concurrency
Swift 6.1 is turning up the dial on concurrency, making it safer and more intuitive for developers (and yes, more fun too!). If you’ve ever been puzzled by where your async code actually runs or found yourself scattering @MainActor
attributes like confetti, Swift 6.1 brings some game-changing tweaks. In this article, we’ll explore two big concurrency changes – including one that keeps your async functions “in their lane” and another that can put your entire app’s code on the main thread by default. Grab a cup of ☕ and let’s dive in, with plenty of examples, analogies, and even a few emojis along the way!
Running Async Functions on the Caller’s Actor (SE‑0461) 🎯
Have you ever called an async function and unexpectedly found it running on a background thread? You’re not alone! Swift’s current behavior (up through Swift 5.x) was a bit surprising: if an async function isn’t explicitly tied to an actor or global actor, it runs on the global concurrent executor — essentially a background thread pool. For example, calling a plain async
function from a SwiftUI view or an actor could hop off to a background thread without you realizing, introducing concurrency where you didn’t intend it. 😱 This confusion has tripped up many developers (myself included), often requiring workarounds or careful @Sendable
annotations to avoid data races.
Swift 6.1 changes this behavior in a major way: nonisolated async functions will run on the caller’s actor by default. In simpler terms, if you call a “plain” async function from the main thread (or from an actor), it stays on that thread/actor unless told otherwise. No more surprise thread hops! This aligns with what most of us intuitively expect — your code runs where you call it — and prevents unexpected concurrent access to data. The Swift Evolution proposal puts it plainly: “nonisolated async functions always run on the caller’s actor by default”.
Why is this a big deal? 🤔
Imagine we have a class method that isn’t isolated to any actor: