JSTAcademy
0 XP
Dashboard
Crash Courses
Concurrency: async/await & Actors
22 min
PhD+195 XP
Crash Courses · PhD

Concurrency: async/await & Actors

Write modern Swift concurrency with async/await, run tasks in parallel with async let, and protect shared state with actors.
22 min read+195 XP on completionCert: Swift Crash Course
Tap any word in the text below to start reading from there.

Concurrency: async/await & Actors

Why async/await?

Before Swift 5.5, async code used completion handlers — nested closures that became impossible to read ("callback hell"). async/await makes async code look and read like synchronous code while remaining non-blocking.

Old way (completion handlers):

func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) { ... }
func fetchPosts(for user: User, completion: @escaping ([Post]) -> Void) { ... }

fetchUser(id: 1) { result in
    switch result {
    case .success(let user):
        fetchPosts(for: user) { posts in
            // now we're 2 levels deep and it gets worse
        }
    case .failure(let error): ...
    }
}

New way (async/await):

let user  = try await fetchUser(id: 1)
let posts = await fetchPosts(for: user)
// Linear, readable, no nesting

---

Writing async Functions

import Foundation

// Simulating a network request
func fetchWeather(for city: String) async throws -> String {
    // await suspends this function without blocking the thread
    try await Task.sleep(for: .milliseconds(100))  // simulate 100ms network delay

    let conditions = ["Sunny", "Cloudy", "Rainy", "Stormy"]
    let temp = Int.random(in: 20...35)
    return "\(city): \(conditions.randomElement()!), \(temp)°C"
}

func fetchAllWeather() async {
    let cities = ["Kingston", "Miami", "London", "Tokyo"]

    // Sequential — waits for each before starting next (slow)
    for city in cities {
        if let weather = try? await fetchWeather(for: city) {
            print(weather)
        }
    }
}

// Start the async work:
Task {
    await fetchAllWeather()
}

---

Parallel Execution with async let

func fetchWeatherParallel() async {
    // All four requests start simultaneously
    async let k = fetchWeather(for: "Kingston")
    async let m = fetchWeather(for: "Miami")
    async let l = fetchWeather(for: "London")
    async let t = fetchWeather(for: "Tokyo")

    // await here — all four complete and we collect results
    let results = await [
        (try? k) ?? "Kingston: unavailable",
        (try? m) ?? "Miami: unavailable",
        (try? l) ?? "London: unavailable",
        (try? t) ?? "Tokyo: unavailable",
    ]

    results.forEach { print($0) }
    // All 4 complete in ~100ms (parallel) vs ~400ms (sequential)
}

Task { await fetchWeatherParallel() }

---

Actors: Thread-Safe Shared State

// Without actor — data race (two threads read/write simultaneously):
// class UnsafeCounter { var count = 0 }  // NOT thread-safe

// With actor — Swift serializes all access:
actor SafeCounter {
    private var count = 0
    private var history: [String] = []

    func increment() {
        count += 1
        history.append("Incremented to \(count)")
    }

    func decrement() {
        count = max(0, count - 1)
    }

    func value() -> Int { count }
    func getHistory() -> [String] { history }
}

// Using an actor requires await — accessing actor state is async:
let counter = SafeCounter()

Task {
    await counter.increment()
    await counter.increment()
    await counter.increment()
    let value = await counter.value()
    print("Counter: \(value)")  // 3
}

---

@MainActor for UI

// In a real iOS app, UI updates must happen on the main thread
@MainActor
class ViewModel: ObservableObject {
    @Published var items: [String] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    func loadItems() async {
        isLoading = true
        defer { isLoading = false }  // defer + async work together

        do {
            // fetch from network (runs on background thread internally)
            let fetched = try await fetchFromServer()
            items = fetched  // safe — we're on MainActor
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    private func fetchFromServer() async throws -> [String] {
        try await Task.sleep(for: .seconds(1))  // simulate network
        return ["Item A", "Item B", "Item C"]
    }
}

---

TaskGroup: Dynamic Concurrency

func fetchMultiple(ids: [Int]) async -> [String] {
    await withTaskGroup(of: String?.self) { group in
        for id in ids {
            group.addTask {
                try? await fetchWeather(for: "City\(id)")
            }
        }
        var results: [String] = []
        for await result in group {
            if let r = result { results.append(r) }
        }
        return results
    }
}

Task {
    let results = await fetchMultiple(ids: [1, 2, 3, 4, 5])
    print("Got \(results.count) results")
}
0%