The previous two posts in this series teach modern concurrency the way we learned it: the fundamentals (async/await, actor, Sendable) and the real-world cases (async let, TaskGroup, cancellation, caches). But there’s a story I didn’t tell in either one: Swift changed the defaults of concurrency, and for the better.
If you migrated a project to Swift 6 and ended up drowning in Sendable warnings about code that was obviously single-threaded, it wasn’t you. The original model optimized for the hard case (genuinely parallel code) and punished the common one (an app that runs almost everything on the main thread). Swift 6.2 flipped that around, and 6.3/6.4 —shown at WWDC 2026— sanded off the remaining rough edges.
This post is the map of those changes: what broke, what improved, and what to switch on in your project today.
The problem: concurrency was hostile by default
Before 6.2 there were two frictions everyone hit.
Friction 1 — Sendable warnings on single-threaded code. A ViewModel that only touches the UI got data-race safety errors, because the compiler assumed nothing about where it ran. The answer was to sprinkle @MainActor everywhere until it went quiet.
Friction 2 — surprise thread hops. A nonisolated async function jumped to the global pool, even when you called it from the @MainActor. This surprised everybody:
@MainActor
func load() async {
let object = NotSendable()
await object.doSomething() // 😱 this did NOT run on the main thread
}
That invisible hop was the source of half the Sendable errors: suddenly your types crossed a concurrency boundary you never asked to cross.
Swift 6.2 — the default flip
The Swift team’s answer was to reorganize concurrency into phases of increasing complexity: first single-threaded code (no concurrency), then async/await with no data-race errors, and only at the end real parallelism for optimization. The defaults now serve phase 1, not phase 3.
Main actor by default (SE-0466)
The biggest change. You can tell the compiler that your whole module is isolated to the @MainActor unless stated otherwise. No more annotating every class.
In Xcode it’s the Default Actor Isolation build setting → MainActor. On the command line it’s the -default-isolation MainActor flag. In a SwiftPM package:
// swift-tools-version: 6.2
.target(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
With that on, this no longer emits a single warning:
// No @MainActor in sight: the whole module is already on the main actor.
@Observable
final class ProfileModel {
var profile: Profile?
func load() async throws {
profile = try await api.profile()
}
}
This is exactly what you want in a UI target or a script. When you do need to leave the main thread, you ask for it explicitly (next section). The default serves 90% of the code; the other 10% you mark by hand.
async functions no longer hop threads (SE-0461)
The second friction got fixed too. A nonisolated async function now runs in its caller’s context instead of jumping to the global executor. The upcoming feature is called NonisolatedNonsendingByDefault:
@MainActor
func load() async {
let object = NotSendable()
await object.doSomething() // ✅ now it DOES run on the main thread
}
And if you want to be explicit about that semantics, you write nonisolated(nonsending). The practical result: the code does what it looks like it does, and most Sendable errors vanish because you’re no longer crossing concurrency boundaries by accident.
@concurrent — the opt-in to leave for the pool
If async functions no longer hop threads by default, how do you send heavy work to the background? With the new @concurrent attribute, which explicitly says “this one does run on the concurrent pool, no matter what”:
@MainActor
final class FeedModel {
func loadFeed() async throws -> Feed {
let (data, _) = try await URLSession.shared.data(from: endpoint)
return try await decode(data) // hops to the background, on purpose
}
@concurrent
private func decode(_ data: Data) async throws -> Feed {
try JSONDecoder().decode(Feed.self, from: data) // heavy parse, off the main thread
}
}
This is the inversion that changes everything: before, you left the main thread by accident and came back to it on purpose; now you stay on it by default and leave on purpose. @concurrent implies nonisolated, so you don’t need both, and it doesn’t combine with @MainActor or nonisolated(nonsending).
Turning it all on
Xcode has an Approachable Concurrency setting that bundles this batch of upcoming features. In SwiftPM you enable them one by one:
// swift-tools-version: 6.2
.target(
name: "MyPackage",
swiftSettings: [
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("InferSendableFromCaptures"),
.enableUpcomingFeature("GlobalActorIsolatedTypesUsability"),
.enableUpcomingFeature("DisableOutwardActorInference"),
]
)
Bonus: debugging that isn’t guesswork anymore
Swift 6.2 also fixed the worst part of concurrency: debugging it. You can now name tasks (Task(name:)) and that name shows up in the debugger and in Instruments; LLDB reliably steps into async functions; and when you stop at a breakpoint you can see which task you’re on.
Swift 6.3 and 6.4 — the polish (WWDC 2026)
If 6.2 fixed the defaults, 6.3 and 6.4 —the ones Apple showed at WWDC 2026— smoothed out the day-to-day. They’re small changes, but each one kills an ugly pattern we all used to write.
await inside defer (SE-0493)
Finally. Cleaning up an async resource from a defer used to be impossible: you had to duplicate the cleanup call on every exit path, or spawn a loose Task. Now:
func importArticles() async throws {
let importer = ArticleImporter()
await importer.open()
defer {
await importer.close() // ✅ valid in Swift 6.4
}
try await importer.importLatest() // if it throws, the close still happens
}
withTaskCancellationShield (SE-0504)
The classic problem: your task gets cancelled, but the cleanup also needs await — and since the task is already cancelled, that await fails or gets skipped. You used to spawn a detached Task just to close properly. Now there’s a shield:
func closeConnection() async {
await withTaskCancellationShield {
// Inside the shield, Task.isCancelled is false:
// the close completes even though we were cancelled.
await database.close()
}
}
The compiler warns when you swallow an error (SE-0520)
This one catches real bugs. A Task { try await … } whose error nobody observes used to vanish silently. Now it’s a warning:
// ⚠️ warning: Unstructured throwing task was not used
Task {
try await importArticles()
}
// ✅ either handle it inside…
Task {
do { try await importArticles() }
catch { log.error("Import failed: \(error)") }
}
// ✅ …or keep the task and check it later
let task = Task { try await importArticles() }
try await task.value
Task with typed throws (SE-0520)
Tasks can now declare a concrete error type, like the rest of the language:
let task: Task<String, URLError> = Task {
throw URLError(.badURL)
}
Async Result (SE-0530)
Result had a catching init for synchronous work, but not for async. We wrote the do/catch by hand. Not anymore:
let result = await Result {
try await importArticles()
}
// result: Result<Void, Error> — no manual do/catch
This pairs beautifully with the partial-errors-in-a-TaskGroup pattern from the previous post: catching the failure inside the child is now one line.
weak let (SE-0481, Swift 6.3)
A weak property forced you to use var, and a var breaks Sendable conformance → so you reached for @unchecked Sendable. With weak let, that lie is over:
final class PreviewController: Sendable {
weak let delegate: PreviewDelegate? // ✅ genuinely Sendable, no @unchecked
}
~Sendable — saying “this is NOT Sendable”, on purpose (SE-0518)
Sometimes a type must not cross concurrency boundaries and you want the compiler to guarantee that, not infer it:
enum ExecutionResult: ~Sendable {
case success
case failure(NonSendableThing)
}
How to migrate without suffering
An order that works:
- Turn on “Approachable Concurrency” for the target. It’s the switch that brings the new defaults.
- Set Default Actor Isolation to
MainActoron your UI and app targets. This is where mostSendablewarnings disappear — not the other way around. - Build and see what complains. With the new defaults, what’s left are real data races, not noise.
- Mark with
@concurrentwhat genuinely should leave the main thread: heavy parsing, image processing, crypto. Start from what Instruments points at, not from intuition. - Leave libraries for last. In SwiftPM packages,
.enableUpcomingFeature(…)goes target by target; you don’t have to migrate everything the same day.
Order matters: a lot of people try to fix the warnings before flipping the defaults, and end up hand-annotating what the compiler was going to assume on its own.
Summary
| Change | Version | What it gives you |
|---|---|---|
| Main actor by default | 6.2 | No more Sendable warnings on single-threaded code |
nonisolated(nonsending) | 6.2 | async functions stop hopping threads behind your back |
@concurrent | 6.2 | The explicit opt-in to send work to the pool |
| Named tasks + LLDB | 6.2 | Debugging concurrency stops being guesswork |
weak let | 6.3 | Real Sendable, no @unchecked |
await in defer | 6.4 | Guaranteed async cleanup |
withTaskCancellationShield | 6.4 | Cleanup finishes even when you’re cancelled |
Warning on ignored Task | 6.4 | Swallowed errors become visible |
Task with typed throws | 6.4 | Concrete error types on tasks |
await Result { } | 6.4 | Capture the outcome without do/catch |
~Sendable | 6.4 | Declare the non-Sendable on purpose |
The full arc tells a clear story: Swift concurrency stopped optimizing for the rare case (massive parallelism) and started optimizing for the real one (an app that lives on the main thread and steps into the background when it has to). If you abandoned a Swift 6 migration out of frustration, this is the moment to try again — the language moved toward you.
If you’re missing the foundation, start with Modern concurrency in Swift, continue with Structured concurrency in practice, and come back here to bring your project’s defaults up to date.
Sources: Apple — What’s new in Swift (WWDC26/262) · Swift.org — Swift 6.2 Released · SwiftLee — Approachable Concurrency in Swift 6.2 · SwiftLee — Swift 6.4: What’s New in Concurrency · InfoQ — Swift 6.4 Brings New Language Features