Swift
04 / 05

Concurrency: async/await & Actors

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