Skip to Content
Course content

32: async/await in Swift

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're at a busy coffee shop. You walk up to the counter, order a vanilla latte, and pay. Now, you have two options. You could stand exactly where you are, staring at the barista, refusing to move or speak to anyone until that latte is in your hand. That's "synchronous" execution—and it's a great way to create a massive line of angry customers behind you. Or, you can take a buzzer, step aside, check your emails, and chat with a friend. You're still "waiting" for the coffee, but you aren't blocking the entire shop from functioning. When the buzzer goes off, you step back up and grab your drink.

In Swift, async/await is that buzzer. It allows your code to pause a heavy task (like downloading a 10MB image) without freezing your entire app's user interface. The thread is freed up to do other things, and once the task finishes, your code picks up exactly where it left off.

Moving Past the "Pyramid of Doom"

Before async/await, we relied on completion handlers—closures that get called when a task finishes. If you had to fetch a user, then fetch their posts, then fetch the comments for those posts, you ended up with code that drifted further and further to the right of your screen. I call this the "Pyramid of Doom." It's hard to read, and it's even harder to handle errors without forgetting to call the completion block in one of your guard statements.

With async/await, that nested mess becomes a linear list of instructions. It looks like synchronous code, but it behaves asynchronously.

Defining and Calling Asynchronous Work

To tell Swift that a function might pause, you mark it with the async keyword. When you call that function, you use await to mark the spot where the execution might suspend.

// This function is marked as async because it fetches data from a server
func fetchUserProfile(userId: String) async throws -> UserProfile {
    let url = URL(string: "https://api.example.com/users/\(userId)")!
    
    // We 'await' the network response. The thread is freed here.
    let (data, _) = try await URLSession.shared.data(from: url)
    
    return try JSONDecoder().decode(UserProfile.self, from: data)
}

Notice the try await. Since network calls can fail, we combine error handling with the suspension point. I've found that this makes the "happy path" of your logic much easier to follow because you aren't hunting through nested braces to see where the error is actually handled.

Orchestrating Multiple Calls

The real magic happens when you have dependent tasks. Let's say you need a user's profile before you can fetch their specific recent photos. In the old way, this was a nightmare of nested closures. Now, it's just this:

func loadDashboardData(userId: String) async {
    do {
        // Step 1: Get the profile
        let profile = try await fetchUserProfile(userId: userId)
        
        // Step 2: Use the profile to get the photos
        let photos = try await fetchUserPhotos(userId: profile.id)
        
        print("Loaded \(photos.count) photos for \(profile.name)")
    } catch {
        print("Something went wrong: \(error)")
    }
}

The code reads top-to-bottom. It's clean. It's predictable. But there's one catch: you can't call an async function from a standard, synchronous function (like viewDidLoad in UIKit or a button action) without a bridge.

Bridging the Gap with Task

When you're in a synchronous environment but need to kick off some asynchronous work, you use a Task. Think of a Task as a wrapper that creates a new asynchronous context.

func handleRefreshButtonTapped() {
    // We are in a sync function, so we wrap the async call in a Task
    Task {
        do {
            await loadDashboardData(userId: "123")
            print("Refresh complete!")
        } catch {
            // Handle the error on the main thread
        }
    }
}

I usually recommend keeping your Task blocks as small as possible. Don't put your entire business logic inside the Task; instead, use the Task to call a dedicated async function. It keeps your code testable and prevents your view controllers from becoming dumping grounds for logic.




📋 Practical Task

Refactoring the Movie Detail Fetcher

You've inherited a piece of legacy code that uses completion handlers to fetch movie details and cast lists. This code is currently a "Pyramid of Doom" and is very difficult to maintain. Your goal is to refactor these functions to use async/await.

Current Legacy Code:

struct Movie { let title: String; let id: Int }
struct Actor { let name: String }

func fetchMovie(id: Int, completion: @escaping (Result<Movie, Error>) -> Void) {
    // Simulated network delay
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        completion(.success(Movie(title: "Inception", id: id)))
    }
}

func fetchCast(movieId: Int, completion: @escaping (Result<[Actor], Error>) -> Void) {
    // Simulated network delay
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        completion(.success([Actor(name: "Leonardo DiCaprio"), Actor(name: "Elliot Page")]))
    }
}

// The "Pyramid of Doom" call site
func loadMoviePage() {
    fetchMovie(id: 101) { movieResult in
        switch movieResult {
        case .success(let movie):
            fetchCast(movieId: movie.id) { castResult in
                switch castResult {
                case .success(let cast):
                    print("Movie: \(movie.title), Cast: \(cast.map { $0.name })")
                case .failure(let error):
                    print("Cast error: \(error)")
                }
            }
        case .failure(let error):
            print("Movie error: \(error)")
        }
    }
}

Your Task:

  1. Rewrite fetchMovie(id:) and fetchCast(movieId:) to be async throws functions (you can replace the DispatchQueue delay with try await Task.sleep(nanoseconds: 1_000_000_000)).
  2. Rewrite the loadMoviePage() function to use a Task block and await both calls linearly.
  3. Ensure all errors are handled using a do-catch block.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.