Skip to content
AgileLogics
7 min read

Async/Await

Prerequisites: Networking with URLSession, Error handling

Some work takes time: network requests, reading large files, database queries. Asynchronous code lets a program keep doing other things — like keeping an app's interface responsive — while it waits.

Real-world analogy: ordering at a café. You place your order (start the task), step aside so others can order (the thread is free), and come back when your name is called (the result is ready).

async and await

  • async marks a function that can suspend — pause while waiting, without blocking the thread it runs on.
  • await marks each point where suspension can happen. You can always see where your code might pause.
func fetchUser(id: Int) async -> String {
    try? await Task.sleep(for: .seconds(1))   // stands in for a network call
    return "User \(id)"
}

print("Fetching...")
let user = await fetchUser(id: 1)
print(user)
Fetching...
User 1

The code reads top to bottom like ordinary code, even though it waits a second in the middle. Before async/await, the same thing needed nested completion-handler closures, which were much harder to read and easy to get wrong.

Throwing async functions

async combines with throws; call them with try await:

enum LoadError: Error { case notFound }

func loadProfile(id: Int) async throws -> String {
    try await Task.sleep(for: .milliseconds(100))
    guard id > 0 else { throw LoadError.notFound }
    return "Profile \(id)"
}

do {
    print(try await loadProfile(id: 7))
    print(try await loadProfile(id: 0))
} catch {
    print("Failed: \(error)")
}
Profile 7
Failed: notFound

Calling async code from synchronous code

You can only use await inside an async context. To start async work from ordinary code — a button tap, for example — create a Task:

func buttonTapped() {
    Task {
        let user = await fetchUser(id: 42)
        print("Loaded \(user)")
    }
}

In SwiftUI, the .task { } view modifier starts async work when a view appears and cancels it automatically when the view disappears.

Sequential by default

Each await waits for its result before the next line runs:

let a = await fetchUser(id: 1)   // 1 second
let b = await fetchUser(id: 2)   // another second

That takes two seconds. When the calls don't depend on each other, run them concurrently — the next lesson shows how.

Common mistakes

  • Calling an async function without await: expression is 'async' but is not marked with 'await'.
  • Wrapping everything in Task { } to silence that error. Unstructured tasks aren't cancelled automatically; make the caller async instead where you can.