iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

async / await

Swift Concurrency replaces nested completion handlers with async/await and structured task trees. Task, actor, and async let compose cleanly; TaskGroup handles parallelism; the compiler enforces data-race safety end to end.

async, Task, actor, TaskGroup, cancellation

EXAMPLE
// 1) Basic — async function + await
import Foundation

func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com/users/\(id)")!)
    return try JSONDecoder().decode(User.self, from: data)
}

// Call from another async function
func loadProfile() async throws {
    let user = try await fetchUser(id: 42)
    print(user.name)
}

// 2) Top-level — wrap in Task
Task {
    do {
        try await loadProfile()
    } catch {
        print("failed: \(error)")
    }
}

// 3) Sequential vs parallel
func sequential() async throws -> (User, Posts) {
    let user = try await fetchUser(id: 1)        // wait
    let posts = try await fetchPosts(for: 1)     // then wait
    return (user, posts)
}

func parallel() async throws -> (User, Posts) {
    async let user  = fetchUser(id: 1)            // start NOW
    async let posts = fetchPosts(for: 1)          // start NOW (in parallel)
    return try await (user, posts)                // wait for both
}

// async let starts the work immediately; await collects results.

// 4) TaskGroup — parallelise N dynamic items
func fetchAll(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await fetchUser(id: id) }
        }
        var results: [User] = []
        for try await user in group {
            results.append(user)
        }
        return results
    }
}

// 5) Bounded concurrency — fixed concurrent inflight
func fetchAllBounded(ids: [Int], maxInFlight: Int = 4) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        var idx = 0
        for _ in 0..<min(maxInFlight, ids.count) {
            let id = ids[idx]; idx += 1
            group.addTask { try await fetchUser(id: id) }
        }
        var results: [User] = []
        for try await user in group {
            results.append(user)
            if idx < ids.count {
                let id = ids[idx]; idx += 1
                group.addTask { try await fetchUser(id: id) }
            }
        }
        return results
    }
}

// 6) Cancellation
let task = Task {
    try await heavyWork()
}
task.cancel()                                      // cooperative; the work must check

func heavyWork() async throws {
    for i in 0..<1_000_000 {
        try Task.checkCancellation()              // throws CancellationError when cancelled
        // ... work ...
    }
}

// All async APIs in the stdlib check cancellation. URLSession data tasks abort on Task.cancel().

// 7) Sleep + delay
try await Task.sleep(for: .seconds(1))            // Swift 5.7+
try await Task.sleep(nanoseconds: 500_000_000)

// 8) Actors — opt-in serial-access classes
actor Counter {
    private var value = 0
    func increment() { value += 1 }
    func current() -> Int { value }
}

let counter = Counter()
await counter.increment()                          // cross-actor calls need await
let v = await counter.current()

// Actors guarantee one thread at a time accesses their state — no data races by construction.

// 9) Main actor — UI work
@MainActor
class ViewModel: ObservableObject {
    @Published var users: [User] = []

    func load() async {
        do {
            users = try await fetchAll(ids: [1, 2, 3, 4])
        } catch {
            // handle
        }
    }
}

// @MainActor methods always run on the main thread; useful for SwiftUI publishers.

// Move work off the main actor:
func backgroundWork() async {
    let data = await Task.detached(priority: .background) {
        return heavyCompute()                       // runs off main actor
    }.value
}

// 10) AsyncSequence — async iteration
for try await line in url.lines {                  // URL.lines is AsyncSequence<String>
    print(line)
}

for try await event in client.subscribe() {
    handle(event)
}

// Build your own:
struct Counter: AsyncSequence, AsyncIteratorProtocol {
    typealias Element = Int
    var current = 0
    let max = 10
    mutating func next() async throws -> Int? {
        guard current < max else { return nil }
        try await Task.sleep(for: .milliseconds(100))
        defer { current += 1 }
        return current
    }
    func makeAsyncIterator() -> Counter { self }
}

for try await n in Counter() { print(n) }

// 11) Continuations — bridge legacy callbacks to async
func fetchLegacy() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        oldAPI(callback: { data, error in
            if let error { continuation.resume(throwing: error) }
            else        { continuation.resume(returning: data!) }
        })
    }
}

// Use checked variants during dev; switch to unchecked after verifying single resume.

// 12) Sendable + data-race safety
// Types crossing actor boundaries must be Sendable:
//   • Value types (struct/enum) with Sendable fields are auto-Sendable
//   • Reference types must be 'final class' + immutable, or actor, or use @unchecked Sendable carefully
//   • Swift 6 enables strict checking by default; Swift 5.10 has incremental flags

@MainActor
struct UserCard: View {
    let user: User                                  // User must be Sendable
    var body: some View { Text(user.name) }
}

// 13) Errors
func parse() async throws -> Config {
    do {
        let data = try await loadData()
        return try JSONDecoder().decode(Config.self, from: data)
    } catch let e as DecodingError {
        throw ConfigError.decode(e)
    }
}

// 14) Patterns
// • Convert Combine pipelines to AsyncSequence with .values
// • Use @MainActor on view models + presenters
// • Detach with Task.detached when you need to escape the current actor's context
// • Cancel tasks in onDisappear or deinit
// • TimeoutTask via a TaskGroup race (race a sleep against the real work)

func withTimeout<T>(seconds: Double, _ work: @escaping @Sendable () async throws -> T) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask { try await work() }
        group.addTask {
            try await Task.sleep(for: .seconds(seconds))
            throw URLError(.timedOut)
        }
        defer { group.cancelAll() }
        if let v = try await group.next() { return v }
        throw URLError(.timedOut)
    }
}

// 15) Common bugs
// • Forgot await — function suspended but not awaited → 'expression is async but not awaited'
// • Calling main-actor method from background → compile warning / error
// • Holding a Mutex across await — deadlock; use actor or @MainActor
// • Capturing self in a Task — strong reference; use [weak self]
// • TaskGroup not awaited — group cancelled when scope ends; collect results first
// • Not checking cancellation in long loops → tasks run after Task.cancel()
// • Detached tasks lose actor context — explicit @MainActor or actor parameter
// • Mixing async/await with completion handlers in the same flow — pick one

Why it matters

Swift Concurrency replaces nested callbacks with linear await. Run independent calls in parallel with async let, fan out N items with TaskGroup, gate UI updates with @MainActor, and let actors serialise mutable state for free. Cooperative cancellation works only if your code calls try Task.checkCancellation() in long loops.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
func fetchUser() async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: URL(string: "https://example.com")!)
    return String(data: data, encoding: .utf8) ?? ""
}
Task {
    let s = try await fetchUser()
    print(s.prefix(40))
}
Try it Yourself »

Exercise

Mark a function as asynchronous.

func fetch() throws -> String { /* … */ }

Test yourself

Q1. await is allowed inside…
Q2. A unit of concurrency is…
Q3. Isolated mutable state in concurrency is provided by…

Discussion

Loading…