tezvyn:

async/await: Write Concurrent Code That Reads Synchronously

AI-drafted, machine-checkedSource: docs.swift.orgadvanced

async/await lets you write asynchronous code that reads like a synchronous story, eliminating callback hell. It's ideal for network requests or file I/O. The footgun is thinking `await` blocks a thread; it only suspends the current task.

WHY IT EXISTS: Before async/await, Swift used completion handlers for asynchronous tasks. Chaining operations, like a network call followed by data parsing, led to nested closures known as "callback hell" or the "pyramid of doom." This code was difficult to read, reason about, and handle errors in consistently.

THE MENTAL MODEL: Think of an async function as one that can pause itself. The async keyword marks a function as pausable. The await keyword is where you actually press pause. When you await a task, your function yields the thread back to the system, saying "I'm waiting for this to finish; feel free to do other work." Once the awaited task completes, your function resumes right where it left off with the result.

HOW IT WORKS: The Swift compiler and runtime work together to manage structured concurrency. An async function is broken into parts at each potential suspension point (await). When a function is suspended, its state is saved, and the thread is returned to a cooperative pool to run other work. This prevents threads from being blocked while waiting for I/O. To run an async function from a synchronous context, you must create a new concurrent context using a Task, like Task { await myFunction() }.

WHEN TO USE IT: Use async/await for nearly all new asynchronous code in Swift. It's ideal for I/O-bound operations like network requests, file access, or database queries. It keeps the UI responsive by easily moving work off the main thread without the complexity of manual thread management with Grand Central Dispatch (GCD).

WHEN NOT TO USE IT: For CPU-bound, highly parallelizable tasks, lower-level APIs like GCD's DispatchQueue.concurrentPerform might still provide more fine-grained control and performance. Also, when working with legacy codebases that heavily use completion handlers, you'll need to write bridging code (e.g., using withCheckedContinuation) rather than replacing everything at once.

ONE CANONICAL EXAMPLE: Fetching a user and then their avatar. Before, this required nested callbacks. With async/await, the code is linear: func fetchUserAndAvatar(userID: String) async throws -> (User, UIImage) { let user = try await api.fetchUser(id: userID) let avatar = try await imageLoader.fetchImage(url: user.avatarURL) return (user, avatar) } This looks synchronous, but await allows the thread to do other work during the network calls, and try/catch handles errors from both operations cleanly.

Read the original → docs.swift.org

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.