Swift Concurrency (async/await) Cheat Sheet
Explains Swift's async/await, structured concurrency with Task and TaskGroup, actors, and async sequences for safe concurrent code.
Async Functions
Declaring and calling asynchronous functions.
// Declaring an async functionfunc fetchUser(id: String) async throws -> User { let url = URL(string: "https://api.example.com/users/\(id)")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data)}// Calling itTask { do { let user = try await fetchUser(id: "42") print(user.name) } catch { print("Failed: \(error)") }}
Creating Tasks
Running concurrent work with async let and unstructured tasks.
// Launch concurrent work with async letasync let user = fetchUser(id: "1")async let posts = fetchPosts(userId: "1")let (u, p) = try await (user, posts) // both run concurrently// Unstructured tasklet task = Task { try await fetchUser(id: "2")}let result = try await task.value// Task with priority and cancellationlet bgTask = Task(priority: .background) { try Task.checkCancellation() return try await fetchUser(id: "3")}bgTask.cancel()
Actors & Concurrency Concepts
Core building blocks of Swift's structured concurrency model.
- actor- Reference type that serializes access to its mutable state, preventing data races
- @MainActor- Global actor that confines a type, function, or property to the main thread, e.g. for UI updates
- TaskGroup- withTaskGroup(of:) runs a dynamic number of child tasks concurrently and collects their results
- Sendable- Protocol marking types safe to pass across concurrency domains without introducing data races
- Task.sleep- try await Task.sleep(for: .seconds(1)) suspends the current task without blocking the thread
- Task.isCancelled- Cooperative cancellation flag checked inside long-running async work to exit early
Actors & Async Sequences
Defining an actor and consuming data concurrently.
actor Counter { private var value = 0 func increment() -> Int { value += 1 return value }}// Reading a network response line by line with AsyncSequencefunc printLines(from url: URL) async throws { let (bytes, _) = try await URLSession.shared.bytes(from: url) for try await line in bytes.lines { print(line) }}// Concurrent fan-out with a throwing task groupfunc fetchAll(ids: [String]) async throws -> [User] { try await withThrowingTaskGroup(of: User.self) { group in for id in ids { group.addTask { try await fetchUser(id: id) } } var users: [User] = [] for try await user in group { users.append(user) } return users }}
Bridging Callbacks with Continuations
Wrapping completion-handler APIs into async functions with checked continuations.
func fetchLegacy(id: String, completion: @escaping (Result<User, Error>) -> Void) { // old callback-based API}func fetchUser(id: String) async throws -> User { try await withCheckedThrowingContinuation { continuation in fetchLegacy(id: id) { result in switch result { case .success(let user): continuation.resume(returning: user) case .failure(let error): continuation.resume(throwing: error) } } }}// Non-throwing variantfunc afterDelay(_ seconds: Double) async -> Void { await withCheckedContinuation { continuation in DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { continuation.resume() } }}
AsyncStream & AsyncThrowingStream
Producing a custom asynchronous sequence from a push-based source.
func ticks(every interval: Duration) -> AsyncStream<Int> { AsyncStream { continuation in let task = Task { var count = 0 while !Task.isCancelled { try? await Task.sleep(for: interval) count += 1 continuation.yield(count) } continuation.finish() } continuation.onTermination = { _ in task.cancel() } }}for await tick in ticks(every: .seconds(1)) { print("tick \(tick)") if tick >= 3 { break } // triggers onTermination cancellation}
Partial Results & Cancellation in TaskGroup
Collecting best-effort results from a throwing task group without aborting on the first failure.
func fetchAllBestEffort(ids: [String]) async -> [User] { await withTaskGroup(of: User?.self) { group in for id in ids { group.addTask { try? await fetchUser(id: id) // swallow per-task errors } } var users: [User] = [] for await result in group { if let user = result { users.append(user) } } return users }}// Cancel remaining siblings as soon as one child throwsfunc firstSuccess(ids: [String]) async throws -> User { try await withThrowingTaskGroup(of: User.self) { group in for id in ids { group.addTask { try await fetchUser(id: id) } } defer { group.cancelAll() } guard let first = try await group.next() else { throw CancellationError() } return first }}
Advanced Concurrency Vocabulary
Terms that come up once you move past basic async/await usage.
- Actor reentrancy- An actor method can suspend at an await and let another call into the actor run before it resumes, so state can change out from under you across suspension points
- @TaskLocal- Property wrapper for values bound per-task-tree, inherited by child tasks, and automatically scoped to withValue(_:operation:)
- @unchecked Sendable- Opt-out escape hatch telling the compiler to trust manual synchronization instead of verifying Sendable conformance
- Task priority inheritance- Child tasks inherit the priority of their parent by default, and priority can escalate if a higher-priority task awaits a lower one
- Cooperative cancellation- Cancelling a Task only sets a flag; long-running work must poll Task.isCancelled or call Task.checkCancellation() to actually stop
- Custom Executor- SE-0392 lets an actor specify a custom SerialExecutor to control which underlying thread/queue its work runs on
- Global actor- A type conforming to GlobalActor (like @MainActor) provides a single shared actor instance usable as an isolation domain across a codebase
Actor Reentrancy Pitfall
Demonstrating why cached state must be re-checked after every await inside an actor.
actor ImageLoader { private var cache: [String: Image] = [:] func image(for url: String) async throws -> Image { if let cached = cache[url] { return cached } // Suspension point: another call to image(for:) with the same url // can interleave here before this one finishes downloading. let data = try await download(url) let image = Image(data: data) // Re-check instead of blindly overwriting — avoids redundant work // and keeps the cache consistent under reentrancy. if let cached = cache[url] { return cached } cache[url] = image return image }}
Mark shared mutable state as an actor instead of guarding it with locks — the compiler enforces safe, isolated access at compile time via async checks, catching data races before they ship.