Swift Concurrency 6.4 — Quick Reference
🟪 Syntax · 🟦 Types/APIs · 🟩 Correct pattern · 🟥 Bug · 🟨 Gotcha · 🧠 Explanation
Quick Recap — The fast mental model
asyncmeans a function may suspend. It does not mean the function runs on a background thread or in parallel.awaitmarks a possible suspension point and an actor hop.- An actor protects its isolated mutable state by serializing access to it.
@MainActorisolates UI-facing state and work to the main actor.Sendabledescribes values safe to transfer across concurrency boundaries.- Cancellation is cooperative: code must check or pass cancellation through.
- Structured tasks are scoped to their parent; unstructured and detached tasks need deliberate lifetime management.
Structured concurrency first
Use async let for a small, fixed number of independent operations. Use a task group for a dynamic number of child operations. Child work finishes or is cancelled with its scope.
func dashboard() async throws -> Dashboard {
async let profile = loadProfile()
async let messages = loadMessages()
return try await Dashboard(profile: profile, messages: messages)
}func loadAll(_ ids: [UUID]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await loadUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}Bound task-group concurrency when the input can be large. Starting one child for every unbounded record can overwhelm memory, sockets, or the server.
Actor isolation and reentrancy
actor TokenStore {
private var token: String?
func currentToken() -> String? { token }
func replace(with newValue: String?) { token = newValue }
}
let store = TokenStore()
let token = await store.currentToken()Calls from outside the actor cross an isolation boundary, so an await is normally required. The actor protects its state; it does not promise a dedicated thread.
An actor can process another message when one of its methods suspends at await. This is actor reentrancy. Re-check state after suspension before relying on an invariant:
actor ImageCache {
private var images: [URL: Image] = [:]
func image(for url: URL) async throws -> Image {
if let cached = images[url] { return cached }
let downloaded = try await download(url)
// Another call may have populated this entry while download suspended.
if let cached = images[url] { return cached }
images[url] = downloaded
return downloaded
}
}If duplicate work is expensive, store an in-flight task or use a single-flight operation. Do not assume an actor method is an uninterruptible transaction across await.
UI isolation with @MainActor
import Observation
@MainActor
@Observable
final class InboxModel {
private(set) var messages: [Message] = []
private(set) var errorText: String?
private let service: any InboxService
init(service: any InboxService) {
self.service = service
}
func refresh() async {
do {
messages = try await service.messages()
errorText = nil
} catch is CancellationError {
// Let view disappearance or a newer request cancel this work.
} catch {
errorText = "Couldn't load messages."
}
}
}SwiftUI's .task is a good place to call await model.refresh(). Do not use DispatchQueue.main.async as a routine substitute for correct actor isolation.
Sendable and strict concurrency
struct SearchRequest: Sendable {
let query: String
let page: Int
}
protocol SearchService: Sendable {
func search(_ request: SearchRequest) async throws -> [SearchResult]
}Sendable is a concurrency-safety contract, not JSON encoding. Value types containing only sendable fields often receive conformance automatically. A mutable class generally needs synchronization or actor isolation; avoid using @unchecked Sendable unless you can prove and document the safety manually.
Move a project toward Swift 6 language mode and complete concurrency checking deliberately. Fix ownership and isolation boundaries instead of suppressing diagnostics. @preconcurrency and nonisolated(unsafe) are migration/interoperability escape hatches, not default fixes.
How do Sendable and Codable differ?
Sendable describes whether a value can safely cross a concurrency boundary; Codable describes conversion to and from an external representation. A type can conform to either, both, or neither. A network DTO may need both, but one conformance does not imply the other.
When is @unchecked Sendable legitimate?
Use it only when the compiler cannot verify a real synchronization guarantee that the type author can prove—for example, all mutable state is protected by a lock or another audited mechanism. Document the invariant near the conformance and test the synchronization boundary. It suppresses checking; it does not add a lock, actor isolation, or safety by itself.
How do you bridge a callback API with a continuation?
Use withCheckedThrowingContinuation when one callback completes exactly once with one success or failure. Resume on every terminal path and never resume twice. If the callback API can emit multiple values or cancellation matters, use an async sequence or a cancellation-aware adapter instead of forcing a stream into a one-shot continuation. A production bridge should propagate task cancellation when the legacy API supports cancelling its operation.
What is priority inversion?
Priority inversion occurs when high-priority work waits on a resource held by lower-priority work, while intermediate-priority work delays the holder from running. Keep critical sections short, avoid blocking waits on the main thread, and prefer structured async coordination over ad-hoc locks where practical. Measure before changing QoS or task priorities; raising every task's priority can hide starvation rather than fix the dependency.
Cancellation patterns
func process(_ records: [Record]) async throws {
for record in records {
try Task.checkCancellation()
try await process(record)
}
}- Prefer APIs that propagate task cancellation to URLSession, task groups, and child work.
- Check cancellation in long loops or between expensive phases.
- Treat
CancellationErroras control flow, not necessarily a user-facing failure. - Do not swallow cancellation and then overwrite newer UI state with stale results.
- Avoid
Task.sleeppolling when a clock, async sequence, or event notification expresses the wait more directly.
Task lifetime choices
| Construct | Inherits parent context? | Use |
|---|---|---|
async let | Yes | A fixed number of scoped child operations |
withTaskGroup | Yes | A dynamic set of scoped child operations |
Task { ... } | Inherits actor context and priority; unstructured lifetime | Start work from synchronous UI/event code and retain/cancel it deliberately |
Task.detached { ... } | Does not inherit actor context, task-local values, or priority | Rare low-level work where independence is intentional |
Do not create a new Task at every layer of an async call chain. Prefer async functions and let the caller own the task lifetime. Avoid detached tasks for normal networking or UI refreshes.
Swift 6.2+ approachable concurrency settings
Swift 6.2 introduced clearer controls for async isolation. Exact behavior depends on enabled language mode and upcoming-feature settings; review project build settings before relying on it.
- With
NonisolatedNonsendingByDefault, anonisolated asyncfunction runs on the caller's actor by default. @concurrentexplicitly opts a function into concurrent execution away from the caller's actor.- Some targets can opt into main-actor default isolation, which fits UI-focused code but changes the isolation assumptions of the module.
// Under the Swift 6.2+ concurrency mode that enables this behavior:
nonisolated(nonsending) func parse(_ data: Data) async throws -> Model {
try JSONDecoder().decode(Model.self, from: data)
}
@concurrent
func computeThumbnail(from image: ImageData) async -> Thumbnail {
// CPU work that should not run as main-actor-isolated work.
Thumbnail(image)
}Do not annotate every function with @concurrent; use it when parallel execution is intended and the data boundary is safe. async by itself is not a background-work annotation.
Swift 6.4 language/library updates
- Swift 6.4 is the current Swift release at the date this folder was reviewed. Xcode 27 includes Swift 6.4 and the iOS 27 SDK.
- Swift 6.4 supports async cleanup inside
defer; cleanup runs before the async function exits. withTaskCancellationShieldis available for narrowly scoped cleanup that must finish even after parent cancellation. Keep shielded work short and bounded.@Observablehas finer-grained change observation APIs, but ordinary SwiftUI state should generally use the normal model and view integration.- New low-level ownership/collection APIs such as
Span,InlineArray,UniqueArray, and noncopyable generics are valuable in specialized performance-sensitive libraries, not a default requirement for app feature code.
func export(_ document: Document) async throws {
let writer = try await openWriter(for: document)
defer {
await withTaskCancellationShield {
await writer.finishAndClose()
}
}
try await writer.write(document.contents)
}Use cancellation shielding only when abandoning cleanup would corrupt a resource or lose required finalization. It is not a way to make an entire request uncancellable.
Common traps
awaitmarks a potential suspension; the function may continue synchronously.- A task running on the main actor can still perform work that suspends and later resumes on the main actor.
- An actor prevents unsynchronized access to its isolated state, not logical races across suspension points.
- A lock or serial queue can be correct but should not be mixed casually with actor-isolated state.
Task.detachedis not just a “background task.” It drops useful parent context.Task {}is not automatically cancelled when an arbitrary object deallocates. Store its handle or use structured lifetime when appropriate.
Key Takeaways
asyncpermits suspension; it does not promise a background thread or parallel execution.- Use structured tasks when possible, and make task lifetime and cancellation ownership explicit.
- Actors protect isolated mutable state, but code must still account for reentrancy across
await. - Treat
Sendableas a data-transfer safety contract; do not silence a boundary with@unchecked Sendablewithout a reviewed invariant. - Choose
@MainActor,@concurrent, and other isolation explicitly for the work and toolchain you support.
