Understanding Async/Await in Swift
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)
}
Async/await isn’t just syntactic sugar - it’s a fundamentally better way to handle concurrency in Swift.