| name | swift-concurrency |
| description | Definitive Swift 6 / 6.2 concurrency and data-race-safety skill for Apple platforms (iOS 26, Xcode 26). Covers actors and reentrancy, AsyncStream/AsyncSequence lifecycle, TaskGroup, Sendable, strict-concurrency diagnostics, Swift 6.2 Approachable Concurrency (default MainActor isolation, @concurrent, Task.immediate), Mutex/Atomic, bridging callbacks/delegates/GCD to async, and runtime isolation crashes. Use for ANY async/await, actor, Sendable, or isolation question, for migrating to Swift 6 strict concurrency, or whenever the query mentions actor, async, await, AsyncStream, TaskGroup, Sendable, nonisolated, MainActor, withCheckedContinuation, @concurrent, or Swift 6. |
| metadata | {"version":"1.1.0"} |
Swift Concurrency (Swift 6 / 6.2)
Production-grade skill for writing, reviewing, fixing, and migrating Swift
concurrent code under Swift 6 strict concurrency on Apple platforms (iOS 26,
Swift 6.2, Xcode 26). Opinionated, because every rule here has broken a real
shipping app:
- Structured concurrency (
async let, TaskGroup, .task) over unstructured Task {}.
- Actors for shared mutable state,
@MainActor for UI state.
Mutex / Atomic for synchronous, performance-critical, or C-bridging critical sections.
- Compile-time isolation over runtime hops;
@concurrent for explicit background work.
withCheckedContinuation over withUnsafeContinuation; checked variants catch misuse.
- No GCD (
DispatchQueue, DispatchGroup, DispatchSemaphore) in new code — it has no data-race safety guarantees.
Swift 6.2 is the inflection point. "Approachable Concurrency" flips the
model from "opt into safety" to "safe by default, opt into concurrency": with
default MainActor isolation, code runs on the main actor unless you say
otherwise, nonisolated async functions stay on the caller's actor, and you
request background execution explicitly with @concurrent. Read
references/swift-6-2-migration.md before migrating — several 6.2 changes
alter runtime behavior silently.
The Concurrency Layers
Application -> Structured concurrency: async let, TaskGroup, .task modifier
Actor -> actor for shared mutable state; @MainActor for UI state
Async/Await -> Cooperative, non-blocking suspension on the cooperative pool
Sendable -> Compile-time data-race safety at isolation-boundary crossings
Synchronization-> Mutex / Atomic / OSAllocatedUnfairLock for sync critical sections
Compiler -> SWIFT_STRICT_CONCURRENCY=complete, Swift 6 language mode, TSan
Decision Tree 1 — What isolation does this type need?
Does this type own mutable state shared across concurrency domains?
├─ YES → Does it touch UI?
│ ├─ YES → @MainActor class/struct
│ └─ NO → Does access need to suspend (await) or is the state complex?
│ ├─ YES → actor
│ └─ NO → Mutex<State> (iOS 18+) or OSAllocatedUnfairLock (iOS 16+),
│ or Atomic<Value> for a single counter/flag
└─ NO → Value type with only Sendable stored properties?
├─ YES → Implicitly Sendable (internal types only; public needs explicit conformance)
└─ NO → Redesign as a value type, or contain the non-Sendable type
inside a single isolation domain (an actor that mediates all access)
Decision Tree 2 — async let vs TaskGroup vs Task {}?
How many concurrent operations?
├─ Known at compile time (2–5) → async let (simplest; heterogeneous return types OK)
├─ Dynamic count (N items) → TaskGroup
│ ├─ Need results? → withThrowingTaskGroup (iterate with `for try await`)
│ ├─ Fire-and-forget? → withDiscardingTaskGroup (no result-accumulation leak)
│ └─ Unbounded input? → THROTTLE: cap in-flight tasks to activeProcessorCount
└─ Fire-and-forget from a sync context?
├─ Need caller's isolation → Task { } (inherits the actor context)
└─ Need detached, no isolation → Task.detached { }
⚠️ detached strips priority, task-locals, and cancellation propagation. Rarely correct.
Decision Tree 3 — Is this safe to call from an async context?
Does the API block the calling thread?
├─ YES (semaphore.wait, group.wait, Thread.sleep, sync file I/O, NSLock held long)
│ → NEVER in an async context. The cooperative pool is capped at CPU core count
│ (4–10 threads on iPhone); blocking one thread while awaiting work that needs
│ the same pool deadlocks under load.
│ → Wrap the blocking call in DispatchQueue.global() + withCheckedContinuation
│ to move it off the cooperative pool.
└─ NO → Is the work CPU-bound and > ~1 ms?
├─ YES → @concurrent func (Swift 6.2+) to force the global concurrent executor
└─ NO → Safe in the async context
Code-Generation Rules (always apply)
Whether reviewing, generating, or refactoring, every output must be
data-race-free, deadlock-free, and clean under Swift 6 strict concurrency:
- Never block the cooperative pool — no
semaphore.wait(), group.wait(), Thread.sleep(), DispatchQueue.sync, or synchronous file I/O in any async context.
- Resume every continuation exactly once on every code path (success, failure, early
return, cancellation). Use withCheckedThrowingContinuation.
- Re-check actor state after every
await — actors are reentrant at suspension points; the world can change while you await.
AsyncStream cleans up in onTermination for EVERY external resource registered in its setup closure (NotificationCenter observers, delegate = self, timers, KVO, Combine subscriptions). deinit is not enough — a stream can outlive or be dropped independently of its owner. Pair every registration with a symmetric teardown, and call continuation.finish() there for infinite observer streams.
- Use
withDiscardingTaskGroup for fire-and-forget child tasks to avoid result-accumulation leaks.
- Mark public types with explicit
Sendable — conformance is not inferred across module boundaries.
- Throttle unbounded
TaskGroup work — cap in-flight addTask calls to ProcessInfo.processInfo.activeProcessorCount.
- Use
@MainActor annotation for UI isolation, not await MainActor.run { } sprinkled at call sites.
- Handle
CancellationError silently — never surface "cancelled" to the user.
- Inject a
Clock for time-dependent code — never hardcode Task.sleep in logic you need to test.
- Understand Swift 6.2 caller-side isolation —
nonisolated async now inherits the caller's actor; use @concurrent for explicit background.
- Never put a lock inside an actor (double synchronization) and never hold a lock across
await (deadlock).
- No GCD in new code —
DispatchQueue/DispatchGroup/DispatchSemaphore give no data-race safety. Use async/await, actors, TaskGroup, Mutex.
Swift 6.2 Approachable Concurrency — the short version
Swift 6.2 ships a bundle of changes (collectively "Approachable Concurrency")
that make strict concurrency far easier to adopt. The headline shifts:
| Change | Before (6.0/6.1) | After (6.2) | SE |
|---|
| Default isolation | Annotate @MainActor everywhere | Opt-in mode infers @MainActor on all unannotated code | SE-0466 |
nonisolated async execution | Hops to the global concurrent executor | Stays on the caller's actor (nonisolated(nonsending)) | SE-0461 |
| Force background work | Task.detached / manual nonisolated funcs | @concurrent attribute | SE-0461 |
@MainActor type + non-isolated protocol | Compiler error / workarounds | Isolated conformance: extension T: @MainActor P | — |
| Latency-sensitive start | Enqueued Task {} | Task.immediate { } starts synchronously on the current actor | SE-0472 |
Observe @Observable async | Manual plumbing | Observations { model.x } as an AsyncSequence | SE-0475 |
| Async cleanup | No await in defer | defer { await … } allowed | SE-0493 |
sending parameters | n/a | Transfer ownership across isolation boundaries | SE-0430 |
In Xcode, Approachable Concurrency (the bundled upcoming-feature flags) and
Default Actor Isolation = MainActor are separate build settings. Enable
default MainActor isolation for app/executable targets only — never for
library/framework targets, which must stay actor-agnostic for their consumers.
final class StickerLibrary { static let shared = StickerLibrary() }
final class StickerModel { var selection: [PhotosPickerItem] = [] }
struct ContentView: View { @State private var model = StickerModel(); var body: some View { … } }
nonisolated struct JSONParser {
@concurrent func parse(_ data: Data) async throws -> [Record] { }
}
Full detail, the SE-by-SE compatibility table, the silent-runtime-change
warnings, and the staged migration plan live in
references/swift-6-2-migration.md.
Workflows
Audit an existing codebase
- Check
SWIFT_STRICT_CONCURRENCY and Swift language version (references/compiler-flags-ci.md).
- Scan for crash patterns: continuation misuse, cooperative-pool blocking, unthrottled
TaskGroup (references/crash-patterns.md).
- Scan for
@unchecked Sendable / nonisolated(unsafe) (references/sendable-and-isolation.md).
- Check actor isolation: reentrancy, deinit access, split isolation (references/sendable-and-isolation.md).
- Check
AsyncStream: missing finish(), infinite sequences, no onTermination cleanup (references/async-streams-and-bridging.md).
- Rank findings by severity (🔴 crash → 🟡 hang → 🟠 data race → 🟢 best practice) and fix in that order.
Migrate a module to strict concurrency
A three-step progression, not a binary flip — jumping straight to Swift 6
mode buries you in errors with no incremental path:
| Step | Setting | Effect |
|---|
| 1. Targeted | SWIFT_STRICT_CONCURRENCY=targeted | Warnings only at Sendable/@preconcurrency boundaries. Low-noise entry. |
| 2. Complete | SWIFT_STRICT_CONCURRENCY=complete | All checks as warnings. Drive each module to zero. Still ships. |
| 3. Swift 6 mode | SWIFT_VERSION=6.0 | Promotes warnings to errors. Only after the module is clean under complete. |
Migrate bottom-up (leaf modules first), one PR per module. Wrap
non-Sendable third-party types in actors. Full plan, the 6.0→6.2 behavioral
changes, and per-target SPM flags: references/swift-6-2-migration.md.
Build a new concurrent feature
- Pick isolation with Decision Tree 1; pick structure with Decision Tree 2.
- Implement with
Sendable-clean types (references/sendable-and-isolation.md).
- Add cooperative cancellation (references/cancellation-and-tasks.md).
- Inject a
Clock; write deterministic tests with withMainSerialExecutor (references/cancellation-and-tasks.md).
Fix a production crash / hang
- Match the crash signature to a pattern (references/crash-patterns.md).
- If it's a warning-free build crashing at runtime (
_dispatch_assert_queue_fail, _swift_task_checkIsolatedSwift), go to the runtime-isolation diagnostic in references/crash-patterns.md.
- Reproduce with a targeted test; verify the fix under TSan +
LIBDISPATCH_COOPERATIVE_POOL_STRICT=1.
Loop breakers (when a fix cascades)
If you fail to fix the same diagnostic twice, stop and break the loop:
- Sendable spiral (one conformance cascades into 20+ errors): use
@preconcurrency import for the offending module and log a follow-up. It suppresses Sendable checks at the module boundary while preserving local safety — the sanctioned escape hatch, unlike @unchecked Sendable which deletes all checking.
- Actor-isolation cascade (adding
@MainActor to a class breaks every call site): mark individual methods @MainActor first to limit the blast radius.
- Third-party SDK blocker (a type cannot be made
Sendable): wrap it in a dedicated actor that owns the instance and mediates all access.
- TaskGroup OOM (thousands of child tasks): add a sliding-window throttle — wait for
group.next() once in-flight count hits activeProcessorCount.
Confidence checklist (before finalizing)
[ ] No cooperative-pool blocking — no semaphore.wait/Thread.sleep/sync I/O in any async fn
[ ] No continuation leaks — every withChecked*Continuation resumes on every path
[ ] No unjustified @unchecked Sendable / nonisolated(unsafe) — each has documented synchronization
[ ] Actor reentrancy handled — state re-checked after every await
[ ] AsyncStream cleanup — onTermination tears down every registered resource; finish() for infinite streams
[ ] TaskGroup bounded — child-task count throttled for unbounded input
[ ] MainActor correct — UI state @MainActor-isolated; no heavy sync work on MainActor; no MainActor.run anti-pattern
[ ] Swift 6.2 ready — nonisolated(nonsending) understood; @concurrent where background is required
[ ] Cancellation handled — withTaskCancellationHandler for long ops; CancellationError caught silently
[ ] Tests deterministic — Clock injected, withMainSerialExecutor used, no flaky timing
[ ] Synchronization correct — no lock inside an actor; no lock held across await
[ ] Compiler flags — SWIFT_STRICT_CONCURRENCY=complete; TSan in CI
Anti-rationalizations
| Thought | Reality |
|---|
"Just add @MainActor and it works." | @MainActor has isolation-inheritance rules; blanket application moves the failure to a runtime trap. |
"I'll silence it with nonisolated(unsafe)." | That deletes the compiler's evidence of a race; the runtime assertion still fires on device. Legit only for a value provably never accessed concurrently. |
| "It's just one async call." | Even one await introduces cancellation and reentrancy implications. |
| "I know how actors work." | Reentrancy and isolation rules changed in Swift 6.2. |
| "My Swift 6 build has zero warnings, so isolation is correct." | Static checking can't see SDK callbacks; the runtime check crashes anyway (crash-patterns.md). |
"async means it runs on a background thread." | async suspends without blocking but resumes on the same actor. Use @concurrent to force background. |
| "Combine is dead, rewrite it as async/await." | Combine has no deprecation; rewriting working pipelines wastes effort. |
References
Start with crash-patterns.md, sendable-and-isolation.md, and
swift-6-2-migration.md for most tasks; consult the rest by workflow.
| Reference | When to read |
|---|
| references/swift-6-2-migration.md | Approachable Concurrency in full: SE-by-SE table, 5.x→6.0→6.2 migration plan, silent runtime behavior changes, @preconcurrency runtime crashes, default-isolation per target. |
| references/sendable-and-isolation.md | Actor isolation rules, reentrancy, in-flight request coalescing, custom actor executors (unownedExecutor on a serial queue), nonisolated deinit / isolated deinit, Sendable rules, sending, region-based isolation, @unchecked risks, isolated conformances, split isolation, whole-protocol vs per-requirement @MainActor, -O/-Onone archive-only isolation divergence, @Observable + nonisolated(unsafe) needs @ObservationIgnored, C++ pointer across await, ABI-stable annotation table. |
| references/crash-patterns.md | Production crash & hang patterns with exact signatures: continuation misuse, cooperative-pool deadlocks, TaskGroup OOM, opaque framework blocking, watchdog kills (0x8badf00d), release-only crashes, SwiftData/Core Data threading, plus the runtime-isolation diagnostic (_dispatch_assert_queue_fail / _swift_task_checkIsolatedSwift). |
| references/structured-concurrency.md | async let, TaskGroup variants (non-throwing group silently discards child throws), throttling, @concurrent ≠ cooperative — chunk long sync compute + Task.yield(), @Observable + concurrency, SwiftUI .task, common mistakes. |
| references/synchronization-primitives.md | Mutex (iOS 18+), OSAllocatedUnfairLock (iOS 16+), Atomic + memory ordering, locks-vs-actors decision guide, anti-patterns. |
| references/cancellation-and-tasks.md | Cooperative cancellation, monotonic request-ID guard for out-of-order responses, withTaskCancellationHandler, timeouts, task retain cycles, nonisolated(unsafe) Task/Timer cancel in a @MainActor deinit, Task.immediate UI-ordering, deterministic async testing (Clock injection, withMainSerialExecutor, Swift Testing). |
| references/async-streams-and-bridging.md | AsyncStream/AsyncSequence lifecycle & backpressure, onTermination cleanup, [weak self] is a no-op for for await, Combine-Subject→AsyncStream in an @Observable, HTTP streaming + SSE parser (bytes.lines), WebSocket actor + auto-reconnect, checked continuations, delegate bridging, annotating imported C/ObjC/C++ headers (NS_SWIFT_*), GCD→async migration table, swift-async-algorithms. |
| references/compiler-flags-ci.md | SWIFT_STRICT_CONCURRENCY, per-target SPM flags + language-MODE staging / dual manifest, Swift 6.2 feature flags, swift package migrate --to-feature, concurrency-annotation build-TIME cost (Debug targeted vs CI complete), extract warnings as audit ground truth, TSan / LIBDISPATCH_COOPERATIVE_POOL_STRICT, lint rules, CI pipeline. |
| references/diagnostics-fix-mapping.md | Compiler error → ordered fixes: "Sending X risks data races", "non-sendable capture", "static property not concurrency-safe", isolation/conformance errors, async-without-await. |