Labs ICT
Pro Login

Async/Await

Modern concurrency in Swift.

Async/Await in Swift

Before async/await, asynchronous code in Swift relied on completion handlers and callbacks. It worked, but deeply nested callbacks — known as callback hell — made code hard to read. Async/await lets you write asynchronous code that looks and reads like regular synchronous code.

func fetchUserData() async throws -> User {
    let url = URL(string: "https://api.example.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}
Try it Yourself ->

async Functions

Mark a function with async to say it might need to wait for something — a network request, reading a file, or any long-running operation. The function can suspend at any await point and resume later.

func slowOperation() async -> String {
    // Simulate some async work
    try? await Task.sleep(nanoseconds: 2_000_000_000)
    return "Done!"
}

func callSlow() async {
    let result = await slowOperation()
    print(result)  // Done!
}
Try it Yourself ->

The await Keyword

Every time you call an async function, you need await before it. This is your reminder that the function might not return immediately. You can only use await in an async context — another async function or a Task.

func getUserProfile(id: Int) async throws -> Profile {
    let user = try await fetchUser(id: id)
    let posts = try await fetchPosts(for: user)
    let avatar = try await downloadImage(user.avatarURL)
    return Profile(user: user, posts: posts, avatar: avatar)
}
Try it Yourself ->

Task for Concurrent Work

A Task lets you start async work from anywhere — even from synchronous code. Fire off multiple tasks and they run concurrently, giving you a huge performance boost.

func loadData() {
    Task {
        let user = try await fetchUser()
        print(user)
    }

    Task {
        let posts = try await fetchPosts()
        print(posts)
    }
    // Both run at the same time
}
Try it Yourself ->

TaskGroup for Parallel Operations

When you need to run multiple similar operations and collect the results, TaskGroup is your friend. It manages a group of child tasks and lets you iterate over results as they arrive.

func fetchAllUsers(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                try await self.fetchUser(id: id)
            }
        }

        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}
Try it Yourself ->

Actors for Thread Safety

Shared mutable state is a common source of bugs. Actors protect their state by ensuring only one task accesses it at a time. No locks, no race conditions — Swift handles synchronization for you.

actor BankAccount {
    let id: Int
    private(set) var balance: Double

    init(id: Int, balance: Double) {
        self.id = id
        self.balance = balance
    }

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
}

let account = BankAccount(id: 1, balance: 1000)
await account.deposit(500)
let success = await account.withdraw(200)
print(await account.balance)  // 1300
Try it Yourself ->

Why Async/Await Replaced Completion Handlers

Completion handlers work, but they scatter logic across multiple closures. Async/await puts everything in one linear flow. Error handling with do/try/catch is cleaner, control flow is straightforward, and the code actually tells a story.

// Before (completion handler)
func fetchDataOld(completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(NSError(domain: "", code: 0)))
            return
        }
        completion(.success(data))
    }.resume()
}

// After (async/await)
func fetchData() async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}
Try it Yourself ->