Chapter 2 — async/await fundamentals

The problem, in the old shape

// The old way — nested, error handling scattered, easy to get wrong.
func loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
    URLSession.shared.dataTask(with: profileURL) { data, response, error in
        if let error { completion(.failure(error)); return }
        guard let data else { completion(.failure(FetchError.noData)); return }
        do {
            let profile = try JSONDecoder().decode(Profile.self, from: data)
            completion(.success(profile))
        } catch {
            completion(.failure(error))
        }
    }.resume()   // ← forget this and nothing happens, silently
}

The same logic with async/await

// Fetchr/ProfileLoader.swift
import Foundation

struct Profile: Decodable {
    let name: String
    let avatarURL: URL
}

func loadProfile() async throws -> Profile {
    let (data, _) = try await URLSession.shared.data(from: profileURL)
    return try JSONDecoder().decode(Profile.self, from: data)
}

async: a function that can suspend

  • another async function, or
  • a Task (which we meet in Chapter 3 — it's how you bridge from ordinary synchronous code).

await: a possible suspension point

await marks a point where the function might suspend. Suspending means the function pauses and gives its thread back to the system, which is free to run other work. When the awaited result is ready, the function resumes — possibly on a different thread — and continues.

flowchart TB subgraph BLOCK__Blocking__old___ [\"Blocking (old)\"] B1["thread starts loadData"] --> B2["thread WAITS
(frozen, wasted)"] --> B3["thread continues"] end subgraph SUSP__Suspending__await___ [\"Suspending (await)\"] S1["thread starts loadData"] --> S2["await: thread RELEASED
→ runs other work"] S2 -. result ready .-> S3["a thread resumes loadData"] end
  • Between await and resumption, the world can change. Because the thread went off and did other
  • You may resume on a different thread. Async functions are not tied to a thread. (They can be

Sequential awaits are still sequential

// Fetchr/AvatarLoader.swift
func loadProfileAndAvatar() async throws -> (Profile, UIImage) {
    let profile = try await loadProfile()                       // (1) waits for profile
    let (data, _) = try await URLSession.shared.data(from: profile.avatarURL)  // (2) THEN avatar
    guard let image = UIImage(data: data) else { throw FetchError.badImage }
    return (profile, image)
}

Bringing it into the app: URLSession's async API

func data(from url: URL) async throws -> (Data, URLResponse)
// Fetchr/FetchError.swift
enum FetchError: Error { case noData, badImage, badStatus(Int) }

// Fetchr/APIClient.swift
struct APIClient {
    func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
            throw FetchError.badStatus((response as? HTTPURLResponse)?.statusCode ?? -1)
        }
        return try JSONDecoder().decode(T.self, from: data)
    }
}

Async properties and async in for loops

// A read-only async property.
var currentUser: User {
    get async throws { try await fetch(User.self, from: meURL) }
}

// Awaiting each element of an async sequence (Chapter 8).
for try await line in fileHandle.bytes.lines {
    print(line)   // suspends between lines as they arrive
}

Wiring it to a button (a peek at Task)

// Fetchr/ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var profile: Profile?
    private let client = APIClient()

    var body: some View {
        VStack {
            if let profile { Text(profile.name) }
            Button("Load") {
                Task {                                  // bridge sync → async
                    profile = try? await client.fetch(Profile.self, from: profileURL)
                }
            }
        }
    }
}

What we built in this chapter

  • Fetchr, whose networking went from a nested completion-handler pyramid to a few flat
  • A precise understanding of async (a function that can suspend, callable only from async
  • The two consequences of suspension: the world can change across an await, and **you may resume
  • The fact that sequential awaits run in order — concurrency between independent operations is
  • How async/await lets asynchronous code reuse Swift's ordinary error handling and control flow,

Mental model to take away

  • await = "might pause here and give the thread back," not "block." Suspended work occupies no
  • Async functions are a different "color" — callable only from other async code or a Task — and
  • Across an await, assume the world changed and the thread changed. Code reads straight-line but
  • Async/await's quiet superpower is reusing all of Swift's normal toolstry/catch, for,