Swift Concurrency
Fast Path
Before proposing a fix:
- Analyze
Package.swift or .pbxproj to determine Swift language mode, strict concurrency level, default isolation, and upcoming features. Do this always, not only for migration work.
- Capture the exact diagnostic and offending symbol.
- Determine the isolation boundary:
@MainActor, custom actor, actor instance isolation, or nonisolated.
- Confirm whether the code is UI-bound or intended to run off the main actor.
- Check if
@Observable is in play - it changes how isolation inference works on the type.
Project settings that change concurrency behavior:
| Setting |
SwiftPM (Package.swift) |
Xcode (.pbxproj) |
| Language mode |
swiftLanguageVersions or -swift-version (// swift-tools-version: is not a reliable proxy) |
Swift Language Version |
| Strict concurrency |
.enableExperimentalFeature("StrictConcurrency=targeted") |
SWIFT_STRICT_CONCURRENCY |
| Default isolation |
.defaultIsolation(MainActor.self) |
SWIFT_DEFAULT_ACTOR_ISOLATION |
| Upcoming features |
.enableUpcomingFeature("NonisolatedNonsendingByDefault") |
SWIFT_UPCOMING_FEATURE_* |
If any of these are unknown, ask the developer to confirm them before giving migration-sensitive guidance. Do not guess.
Guardrails:
- Do not recommend
@MainActor as a blanket fix. Justify why the code is truly UI-bound.
- Prefer structured concurrency over unstructured tasks. Use
Task.detached only with a clear reason.
- If recommending
@preconcurrency, @unchecked Sendable, or nonisolated(unsafe), require a documented safety invariant and a follow-up removal plan.
- Optimize for the smallest safe change. Do not refactor unrelated architecture during migration.
- When
@Observable is involved, understand that it does NOT automatically imply @MainActor isolation - the developer must explicitly opt in. Do not conflate the two.
Quick Fix Mode
Use Quick Fix Mode when all of these are true:
- The issue is localized to one file or one type.
- The isolation boundary is clear.
- The fix can be explained in 1-2 behavior-preserving steps.
Skip Quick Fix Mode when any of these are true:
- Build settings or default isolation are unknown.
- The issue crosses module boundaries or changes public API behavior.
- The likely fix depends on unsafe escape hatches.
Common Diagnostics
| Diagnostic |
First check |
Smallest safe fix |
Escalate to |
Main actor-isolated ... cannot be used from a nonisolated context |
Is this truly UI-bound? |
Isolate the caller to @MainActor or use await MainActor.run { ... } only when main-actor ownership is correct. |
references/actors.md, references/threading.md |
Actor-isolated type does not conform to protocol |
Must the requirement run on the actor? |
Prefer isolated conformance (e.g., extension Foo: @MainActor SomeProtocol); use nonisolated only for truly nonisolated requirements. |
references/actors.md |
Sending value of non-Sendable type ... risks causing data races |
What isolation boundary is being crossed? |
Keep access inside one actor, or convert the transferred value to an immutable/value type. |
references/sendable.md, references/threading.md |
@Observable class with @MainActor property issues |
Is the entire class UI-bound, or just some properties? |
Apply @MainActor to the whole class if it's a view model; use nonisolated for properties that don't need main actor. |
references/observable.md |
@Observable + async access from background |
Are you mutating observed properties off the main actor? |
Route mutations through await MainActor.run {} or isolate the class to @MainActor. |
references/observable.md |
SwiftLint async_without_await |
Is async actually required by protocol, override, or @concurrent? |
Remove async, or use a narrow suppression with rationale. Never add fake awaits. |
references/linting.md |
wait(...) is unavailable from asynchronous contexts |
Is this legacy XCTest async waiting? |
Replace with await fulfillment(of:) or Swift Testing equivalents. |
references/testing.md |
| Core Data concurrency warnings |
Are NSManagedObject instances crossing contexts or actors? |
Pass NSManagedObjectID or map to a Sendable value type. |
references/core-data.md |
Thread.current unavailable from asynchronous contexts |
Are you debugging by thread instead of isolation? |
Reason in terms of isolation and use Instruments/debugger instead. |
references/threading.md |
| SwiftLint concurrency-related warnings |
Which specific lint rule triggered? |
Use references/linting.md for rule intent and preferred fixes; avoid dummy awaits. |
references/linting.md |
When Quick Fixes Fail
- Gather project settings if not already confirmed.
- Re-evaluate which isolation boundaries the type crosses.
- Route to the matching reference file for a deeper fix.
- If the fix may change behavior, document the invariant and add verification steps.
Smallest Safe Fixes
Prefer changes that preserve behavior while satisfying data-race safety:
- UI-bound state: isolate the type or member to
@MainActor.
- Shared mutable state: move it behind an
actor, or use @MainActor only if the state is UI-owned.
- Background work: when work must hop off caller isolation, use an
async API marked @concurrent; when work can safely inherit caller isolation, use nonisolated without @concurrent.
- Sendability issues: prefer immutable values and explicit boundaries over
@unchecked Sendable.
- @Observable view models: isolate the entire class to
@MainActor when it drives UI; use nonisolated for computed properties or methods that don't touch UI state.
@Observable + @MainActor Quick Guide
This is one of the most common pain points in modern SwiftUI apps. The @Observable macro does NOT infer @MainActor - you must explicitly add it when the class drives UI.
The Core Pattern
// ✅ Correct: @MainActor view model with @Observable
@MainActor @Observable
final class ContentViewModel {
var items: [Item] = []
var isLoading = false
func loadItems() async {
isLoading = true
items = await APIClient.fetchItems()
isLoading = false
}
}
SwiftUI Integration (Observation framework)
Use @State (not @StateObject) and @Environment (not @EnvironmentObject) with @Observable:
// ✅ Correct Observation-era SwiftUI
struct ContentView: View {
@State private var viewModel = ContentViewModel()
var body: some View {
List(viewModel.items) { item in
Text(item.name)
}
.task { await viewModel.loadItems() }
}
}
// ✅ Passing via environment
@main
struct MyApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(library)
}
}
}
struct LibraryView: View {
@Environment(Library.self) private var library
var body: some View {
List(library.books) { book in
BookView(book: book)
}
}
}
Bindings with @Bindable
For two-way bindings to @Observable objects, use @Bindable:
struct EditView: View {
@Bindable var viewModel: ContentViewModel
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
Common Mistakes
// ❌ Missing @MainActor - mutations from async contexts may happen off main thread
@Observable
final class ViewModel {
var items: [Item] = [] // SwiftUI reads this - must be main-actor-isolated
}
// ❌ Using old ObservableObject patterns with @Observable
@Observable
final class ViewModel: ObservableObject { // Don't conform to both
@Published var items: [Item] = [] // @Published is for ObservableObject
}
// ❌ Using @StateObject with @Observable
struct MyView: View {
@StateObject var vm = ViewModel() // Use @State instead
}
For the full guide, see references/observable.md.
Concurrency Tool Selection
| Need |
Tool |
Key Guidance |
| Single async operation |
async/await |
Default choice for sequential async work |
| Fixed parallel operations |
async let |
Known count at compile time; auto-cancelled on throw |
| Dynamic parallel operations |
withTaskGroup |
Unknown count; structured - cancels children on scope exit |
| Sync → async bridge |
Task { } |
Inherits actor context; use Task.detached only with documented reason |
| Shared mutable state |
actor |
Prefer over locks/queues; keep isolated sections small |
| UI-bound state |
@MainActor |
Only for truly UI-related code; justify isolation |
| Observable view model |
@MainActor @Observable |
For SwiftUI-driving models; use @State not @StateObject |
Common Scenarios
Network request with UI update
Task { @concurrent in
let data = try await fetchData()
await MainActor.run { self.updateUI(with: data) }
}
Processing array items in parallel
await withTaskGroup(of: ProcessedItem.self) { group in
for item in items {
group.addTask { await process(item) }
}
for await result in group {
results.append(result)
}
}
Swift 6 Migration Quick Guide
Key changes in Swift 6:
- Strict concurrency checking enabled by default
- Complete data-race safety at compile time
- Sendable requirements enforced on boundaries
- Isolation checking for all async boundaries
Migration Validation Loop
Apply this cycle for each migration change:
- Build - Run
swift build or Xcode build to surface new diagnostics
- Fix - Address one category of error at a time (e.g., all Sendable issues first)
- Rebuild - Confirm the fix compiles cleanly before moving on
- Test - Run the test suite to catch regressions (
swift test or Cmd+U)
- Only proceed to the next file/module when all diagnostics are resolved
If a fix introduces new warnings, resolve them before continuing. Never batch multiple unrelated fixes - keep commits small and reviewable.
For detailed migration steps, see references/migration.md.
Reference Router
Open the smallest reference that matches the question:
- Foundations
references/async-await-basics.md - async/await syntax, execution order, async let, URLSession patterns
references/tasks.md - Task lifecycle, cancellation, priorities, task groups, structured vs unstructured
references/actors.md - Actor isolation, @MainActor, global actors, reentrancy, custom executors, Mutex
references/sendable.md - Sendable conformance, value/reference types, @unchecked, region isolation
references/threading.md - Execution model, suspension points, Swift 6.2 isolation behavior
- Observation
references/observable.md - @Observable + @MainActor patterns, SwiftUI integration, async access, migration from ObservableObject
- Streams
references/async-sequences.md - AsyncSequence, AsyncStream, when to use vs regular async methods
references/async-algorithms.md - Debounce, throttle, merge, combineLatest, channels, timers
- Applied topics
references/testing.md - Swift Testing first, XCTest fallback, leak checks
references/performance.md - Profiling with Instruments, reducing suspension points, execution strategies
references/memory-management.md - Retain cycles in tasks, memory safety patterns
references/core-data.md - NSManagedObject sendability, custom executors, isolation conflicts
- Migration and tooling
references/migration.md - Swift 6 migration strategy, closure-to-async conversion, @preconcurrency, FRP migration
references/linting.md - Concurrency-focused lint rules and SwiftLint async_without_await
- Glossary
references/glossary.md - Quick definitions of core concurrency terms
Verification Checklist
When changing concurrency code:
- Re-check build settings before interpreting diagnostics.
- Build and clear one category of errors before moving on. Do not batch unrelated fixes into the same change.
- Run tests, especially actor-, lifetime-, and cancellation-sensitive tests.
- Use Instruments for performance claims instead of guessing.
- Verify deallocation and cancellation behavior for long-lived tasks.
- Check
Task.isCancelled in long-running operations.
- Never use semaphores or ad hoc locking in async contexts when actor isolation or
Mutex would express ownership more safely.
- When using
@Observable, verify that UI-driving properties are accessed on @MainActor.
1---2name: swift-concurrency3description: Diagnose data races, convert callback-based code to async/await, implement actor isolation patterns, resolve Sendable conformance issues, fix @Observable + @MainActor interaction problems, and guide Swift 6 migration. Use when developers mention: (1) Swift Concurrency, async/await, actors, or tasks, (2) "use Swift Concurrency" or "modern concurrency patterns", (3) migrating to Swift 6, (4) data races or thread safety issues, (5) refactoring closures to async/await, (6) @MainActor, Sendable, or actor isolation, (7) @Observable with concurrency or actor isolation, (8) concurrent code architecture or performance optimization, (9) concurrency-related linter warnings (SwiftLint or similar), (10) SwiftUI view model patterns with Observation framework.4---5# Swift Concurrency67## Fast Path89Before proposing a fix:10111. Analyze `Package.swift` or `.pbxproj` to determine Swift language mode, strict concurrency level, default isolation, and upcoming features. Do this always, not only for migration work.122. Capture the exact diagnostic and offending symbol.133. Determine the isolation boundary: `@MainActor`, custom actor, actor instance isolation, or `nonisolated`.144. Confirm whether the code is UI-bound or intended to run off the main actor.155. Check if `@Observable` is in play - it changes how isolation inference works on the type.1617Project settings that change concurrency behavior:1819| Setting | SwiftPM (`Package.swift`) | Xcode (`.pbxproj`) |20|---|---|---|21| Language mode | `swiftLanguageVersions` or `-swift-version` (`// swift-tools-version:` is not a reliable proxy) | Swift Language Version |22| Strict concurrency | `.enableExperimentalFeature("StrictConcurrency=targeted")` | `SWIFT_STRICT_CONCURRENCY` |23| Default isolation | `.defaultIsolation(MainActor.self)` | `SWIFT_DEFAULT_ACTOR_ISOLATION` |24| Upcoming features | `.enableUpcomingFeature("NonisolatedNonsendingByDefault")` | `SWIFT_UPCOMING_FEATURE_*` |2526If any of these are unknown, ask the developer to confirm them before giving migration-sensitive guidance. Do not guess.2728Guardrails:2930- Do not recommend `@MainActor` as a blanket fix. Justify why the code is truly UI-bound.31- Prefer structured concurrency over unstructured tasks. Use `Task.detached` only with a clear reason.32- If recommending `@preconcurrency`, `@unchecked Sendable`, or `nonisolated(unsafe)`, require a documented safety invariant and a follow-up removal plan.33- Optimize for the smallest safe change. Do not refactor unrelated architecture during migration.34- When `@Observable` is involved, understand that it does NOT automatically imply `@MainActor` isolation - the developer must explicitly opt in. Do not conflate the two.3536## Quick Fix Mode3738Use Quick Fix Mode when all of these are true:3940- The issue is localized to one file or one type.41- The isolation boundary is clear.42- The fix can be explained in 1-2 behavior-preserving steps.4344Skip Quick Fix Mode when any of these are true:4546- Build settings or default isolation are unknown.47- The issue crosses module boundaries or changes public API behavior.48- The likely fix depends on unsafe escape hatches.4950## Common Diagnostics5152| Diagnostic | First check | Smallest safe fix | Escalate to |53|---|---|---|---|54| `Main actor-isolated ... cannot be used from a nonisolated context` | Is this truly UI-bound? | Isolate the caller to `@MainActor` or use `await MainActor.run { ... }` only when main-actor ownership is correct. | `references/actors.md`, `references/threading.md` |55| `Actor-isolated type does not conform to protocol` | Must the requirement run on the actor? | Prefer isolated conformance (e.g., `extension Foo: @MainActor SomeProtocol`); use `nonisolated` only for truly nonisolated requirements. | `references/actors.md` |56| `Sending value of non-Sendable type ... risks causing data races` | What isolation boundary is being crossed? | Keep access inside one actor, or convert the transferred value to an immutable/value type. | `references/sendable.md`, `references/threading.md` |57| `@Observable` class with `@MainActor` property issues | Is the entire class UI-bound, or just some properties? | Apply `@MainActor` to the whole class if it's a view model; use `nonisolated` for properties that don't need main actor. | `references/observable.md` |58| `@Observable` + async access from background | Are you mutating observed properties off the main actor? | Route mutations through `await MainActor.run {}` or isolate the class to `@MainActor`. | `references/observable.md` |59| `SwiftLint async_without_await` | Is `async` actually required by protocol, override, or `@concurrent`? | Remove `async`, or use a narrow suppression with rationale. Never add fake awaits. | `references/linting.md` |60| `wait(...) is unavailable from asynchronous contexts` | Is this legacy XCTest async waiting? | Replace with `await fulfillment(of:)` or Swift Testing equivalents. | `references/testing.md` |61| Core Data concurrency warnings | Are `NSManagedObject` instances crossing contexts or actors? | Pass `NSManagedObjectID` or map to a Sendable value type. | `references/core-data.md` |62| `Thread.current` unavailable from asynchronous contexts | Are you debugging by thread instead of isolation? | Reason in terms of isolation and use Instruments/debugger instead. | `references/threading.md` |63| SwiftLint concurrency-related warnings | Which specific lint rule triggered? | Use `references/linting.md` for rule intent and preferred fixes; avoid dummy awaits. | `references/linting.md` |6465## When Quick Fixes Fail66671. Gather project settings if not already confirmed.682. Re-evaluate which isolation boundaries the type crosses.693. Route to the matching reference file for a deeper fix.704. If the fix may change behavior, document the invariant and add verification steps.7172## Smallest Safe Fixes7374Prefer changes that preserve behavior while satisfying data-race safety:7576- **UI-bound state**: isolate the type or member to `@MainActor`.77- **Shared mutable state**: move it behind an `actor`, or use `@MainActor` only if the state is UI-owned.78- **Background work**: when work must hop off caller isolation, use an `async` API marked `@concurrent`; when work can safely inherit caller isolation, use `nonisolated` without `@concurrent`.79- **Sendability issues**: prefer immutable values and explicit boundaries over `@unchecked Sendable`.80- **@Observable view models**: isolate the entire class to `@MainActor` when it drives UI; use `nonisolated` for computed properties or methods that don't touch UI state.8182## @Observable + @MainActor Quick Guide8384This is one of the most common pain points in modern SwiftUI apps. The `@Observable` macro does NOT infer `@MainActor` - you must explicitly add it when the class drives UI.8586### The Core Pattern8788```swift89// ✅ Correct: @MainActor view model with @Observable90@MainActor @Observable91final class ContentViewModel {92 var items: [Item] = []93 var isLoading = false9495 func loadItems() async {96 isLoading = true97 items = await APIClient.fetchItems()98 isLoading = false99 }100}101```102103### SwiftUI Integration (Observation framework)104105Use `@State` (not `@StateObject`) and `@Environment` (not `@EnvironmentObject`) with `@Observable`:106107```swift108// ✅ Correct Observation-era SwiftUI109struct ContentView: View {110 @State private var viewModel = ContentViewModel()111112 var body: some View {113 List(viewModel.items) { item in114 Text(item.name)115 }116 .task { await viewModel.loadItems() }117 }118}119120// ✅ Passing via environment121@main122struct MyApp: App {123 @State private var library = Library()124125 var body: some Scene {126 WindowGroup {127 LibraryView()128 .environment(library)129 }130 }131}132133struct LibraryView: View {134 @Environment(Library.self) private var library135136 var body: some View {137 List(library.books) { book in138 BookView(book: book)139 }140 }141}142```143144### Bindings with @Bindable145146For two-way bindings to `@Observable` objects, use `@Bindable`:147148```swift149struct EditView: View {150 @Bindable var viewModel: ContentViewModel151152 var body: some View {153 TextField("Name", text: $viewModel.name)154 }155}156```157158### Common Mistakes159160```swift161// ❌ Missing @MainActor - mutations from async contexts may happen off main thread162@Observable163final class ViewModel {164 var items: [Item] = [] // SwiftUI reads this - must be main-actor-isolated165}166167// ❌ Using old ObservableObject patterns with @Observable168@Observable169final class ViewModel: ObservableObject { // Don't conform to both170 @Published var items: [Item] = [] // @Published is for ObservableObject171}172173// ❌ Using @StateObject with @Observable174struct MyView: View {175 @StateObject var vm = ViewModel() // Use @State instead176}177```178179For the full guide, see `references/observable.md`.180181## Concurrency Tool Selection182183| Need | Tool | Key Guidance |184|---|---|---|185| Single async operation | `async/await` | Default choice for sequential async work |186| Fixed parallel operations | `async let` | Known count at compile time; auto-cancelled on throw |187| Dynamic parallel operations | `withTaskGroup` | Unknown count; structured - cancels children on scope exit |188| Sync → async bridge | `Task { }` | Inherits actor context; use `Task.detached` only with documented reason |189| Shared mutable state | `actor` | Prefer over locks/queues; keep isolated sections small |190| UI-bound state | `@MainActor` | Only for truly UI-related code; justify isolation |191| Observable view model | `@MainActor @Observable` | For SwiftUI-driving models; use `@State` not `@StateObject` |192193### Common Scenarios194195**Network request with UI update**196```swift197Task { @concurrent in198 let data = try await fetchData()199 await MainActor.run { self.updateUI(with: data) }200}201```202203**Processing array items in parallel**204```swift205await withTaskGroup(of: ProcessedItem.self) { group in206 for item in items {207 group.addTask { await process(item) }208 }209 for await result in group {210 results.append(result)211 }212}213```214215## Swift 6 Migration Quick Guide216217Key changes in Swift 6:218- **Strict concurrency checking** enabled by default219- **Complete data-race safety** at compile time220- **Sendable requirements** enforced on boundaries221- **Isolation checking** for all async boundaries222223### Migration Validation Loop224225Apply this cycle for each migration change:2262271. **Build** - Run `swift build` or Xcode build to surface new diagnostics2282. **Fix** - Address one category of error at a time (e.g., all Sendable issues first)2293. **Rebuild** - Confirm the fix compiles cleanly before moving on2304. **Test** - Run the test suite to catch regressions (`swift test` or Cmd+U)2315. **Only proceed** to the next file/module when all diagnostics are resolved232233If a fix introduces new warnings, resolve them before continuing. Never batch multiple unrelated fixes - keep commits small and reviewable.234235For detailed migration steps, see `references/migration.md`.236237## Reference Router238239Open the smallest reference that matches the question:240241- Foundations242 - `references/async-await-basics.md` - async/await syntax, execution order, async let, URLSession patterns243 - `references/tasks.md` - Task lifecycle, cancellation, priorities, task groups, structured vs unstructured244 - `references/actors.md` - Actor isolation, @MainActor, global actors, reentrancy, custom executors, Mutex245 - `references/sendable.md` - Sendable conformance, value/reference types, @unchecked, region isolation246 - `references/threading.md` - Execution model, suspension points, Swift 6.2 isolation behavior247- Observation248 - `references/observable.md` - @Observable + @MainActor patterns, SwiftUI integration, async access, migration from ObservableObject249- Streams250 - `references/async-sequences.md` - AsyncSequence, AsyncStream, when to use vs regular async methods251 - `references/async-algorithms.md` - Debounce, throttle, merge, combineLatest, channels, timers252- Applied topics253 - `references/testing.md` - Swift Testing first, XCTest fallback, leak checks254 - `references/performance.md` - Profiling with Instruments, reducing suspension points, execution strategies255 - `references/memory-management.md` - Retain cycles in tasks, memory safety patterns256 - `references/core-data.md` - NSManagedObject sendability, custom executors, isolation conflicts257- Migration and tooling258 - `references/migration.md` - Swift 6 migration strategy, closure-to-async conversion, @preconcurrency, FRP migration259 - `references/linting.md` - Concurrency-focused lint rules and SwiftLint `async_without_await`260- Glossary261 - `references/glossary.md` - Quick definitions of core concurrency terms262263## Verification Checklist264265When changing concurrency code:2662671. Re-check build settings before interpreting diagnostics.2682. Build and clear one category of errors before moving on. Do not batch unrelated fixes into the same change.2693. Run tests, especially actor-, lifetime-, and cancellation-sensitive tests.2704. Use Instruments for performance claims instead of guessing.2715. Verify deallocation and cancellation behavior for long-lived tasks.2726. Check `Task.isCancelled` in long-running operations.2737. Never use semaphores or ad hoc locking in async contexts when actor isolation or `Mutex` would express ownership more safely.2748. When using `@Observable`, verify that UI-driving properties are accessed on `@MainActor`.