Swift: Concurrency — async/await & Actors
Swift 5.5 introduced structured concurrency: async/await, actors, and Tasks. It replaces callback-based and Combine-based async patterns with safer, more readable code.
async/await Basics
import Foundation
// Async function — can be called with await
func fetchUser(id: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw APIError.badResponse
}
return try JSONDecoder().decode(User.self, from: data)
}
// Call an async function
func loadUser() async {
do {
let user = try await fetchUser(id: "123")
print("Loaded: \(user.name)")
} catch APIError.badResponse {
print("Bad response")
} catch {
print("Error: \(error)")
}
}
// Bridge from sync context
Task {
await loadUser()
}
// Async sequence — like AsyncStream
for try await line in url.lines {
print(line)
}Structured Concurrency
// async let — parallel execution
async let user = fetchUser(id: "123")
async let posts = fetchPosts(userId: "123")
async let stats = fetchStats(userId: "123")
// Await all three simultaneously (not sequentially)
let (u, p, s) = try await (user, posts, stats)
// TaskGroup — dynamic parallelism
func fetchAllUsers(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
}
}
// Task — unstructured concurrency (fire and forget or detached)
let task = Task {
try await longRunningWork()
}
// Cancel a task
task.cancel()
// Check for cancellation
func work() async throws {
for item in items {
try Task.checkCancellation() // throws CancellationError if cancelled
await process(item)
}
}Actors
// Actor — reference type that protects mutable state
// Only one caller can access actor internals at a time
actor BankAccount {
private var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
func withdraw(_ amount: Double) throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
}
func getBalance() -> Double {
balance
}
}
let account = BankAccount()
await account.deposit(100)
let balance = await account.getBalance() // must await to access actor
// @MainActor — runs on the main thread (for UI updates)
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
func load() async {
let fetched = await fetchItems()
items = fetched // safe — on MainActor
}
}
// Nonisolated — opt out of actor protection for stateless methods
actor DataProcessor {
nonisolated func formatDate(_ date: Date) -> String {
date.formatted() // no actor isolation needed
}
}Sendable & Data Race Safety
// Sendable — safe to cross actor boundaries
// Value types (structs, enums) are implicitly Sendable
// Classes must be explicitly marked (or @unchecked)
struct Message: Sendable { // value type — Sendable
let id: UUID
let text: String
}
// @Sendable closure
func process(items: [Int], handler: @Sendable (Int) -> Void) {
Task {
items.forEach { handler($0) }
}
}
// AsyncStream — bridge callback-based APIs to async/await
func listenForNotifications() -> AsyncStream<Notification> {
AsyncStream { continuation in
let observer = NotificationCenter.default.addObserver(
forName: .someNotification,
object: nil,
queue: nil
) { notification in
continuation.yield(notification)
}
continuation.onTermination = { _ in
NotificationCenter.default.removeObserver(observer)
}
}
}
// Consume
Task {
for await notification in listenForNotifications() {
handleNotification(notification)
}
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free