Swift’s async/await syntax has transformed how we write asynchronous code. Here’s everything you need to know.

The Problem with Callbacks

Traditional async code often led to “callback hell”:

func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        // ... more nested code
    }.resume()
}

Enter async/await

Swift’s modern concurrency model makes async code read like synchronous code:

func fetchData() async throws -> Data {
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw APIError.invalidResponse
    }
    return data
}

Structured Concurrency with Task

func loadUserProfile() async {
    do {
        async let user = fetchUser()
        async let posts = fetchPosts()
        async let stats = fetchStats()
        
        let profile = try await Profile(
            user: user,
            posts: posts,
            stats: stats
        )
        self.displayProfile(profile)
    } catch {
        self.showError(error)
    }
}

Actors for Thread Safety

actor ImageCache {
    private var cache: [URL: UIImage] = [:]
    
    func image(for url: URL) -> UIImage? {
        cache[url]
    }
    
    func setImage(_ image: UIImage, for url: URL) {
        cache[url] = image
    }
}

MainActor for UI Updates

@MainActor
func updateUI(with data: Data) {
    // Safe to update UI here
    self.label.text = String(data: data, encoding: .utf8)
}

Concurrency diagram

Async/await isn’t just syntactic sugar - it’s a fundamentally better way to handle concurrency in Swift.