Swift Concurrency

Swift's async/await and Task API let you write asynchronous code that reads like ordinary sequential code while the runtime handles suspension and scheduling.

Why Structured Concurrency?

Before async/await, asynchronous Swift code relied on nested completion handlers that were hard to read and easy to get wrong. Marking a function async lets it suspend at await points without blocking the thread, and the compiler enforces that every suspension point is visible at the call site.

An Async Function

struct NetworkError: Error {}

func fetchUserName(id: Int) async throws -> String {
    // Simulates a network call
    try await Task.sleep(nanoseconds: 500_000_000)
    guard id > 0 else { throw NetworkError() }
    return "user_\(id)"
}

func loadProfile(id: Int) async throws -> String {
    let name = try await fetchUserName(id: id)
    return "Profile for \(name)"
}

Launching Work with Task

await can only be used inside an async context. Task { } bridges from ordinary synchronous code into the async world, starting new unstructured work that begins running immediately without blocking the line after it.

Starting Async Work From Sync Code

func startLoading() {
    Task {
        do {
            let profile = try await loadProfile(id: 7)
            print(profile) // Profile for user_7
        } catch {
            print("Failed to load: \(error)")
        }
    }
    print("Task launched, continues without waiting")
}

startLoading()

Running Work in Parallel with async let

A plain await runs one call at a time. async let starts several async calls concurrently and lets execution continue until you actually need their results, at which point await collects them together.

Concurrent Calls With async let

func loadDashboard() async throws {
    async let userName = fetchUserName(id: 1)
    async let userName2 = fetchUserName(id: 2)

    // Both calls run concurrently; execution pauses here
    // until both finish.
    let results = try await [userName, userName2]
    print(results) // ["user_1", "user_2"]
}

Task {
    try await loadDashboard()
}

Task Cancellation and Priority

Tasks can be cancelled cooperatively — cancellation doesn't stop execution automatically, but well-behaved async code checks for it and exits early. Tasks can also be given a priority hint, such as .background or .userInitiated, to influence scheduling.

  • await can only be called from an async function or inside a Task
  • async functions can also throw, so pair them with try when they do
  • await suspends the current task without blocking the underlying thread
  • Structured concurrency (Task, async let, TaskGroup) ensures child work finishes or is cancelled before its parent scope exits
ToolPurposeRuns Concurrently?
Task { }Bridge from sync to async code, start unstructured workYes, independent of the caller
async letRun a fixed, known number of async calls in parallelYes, within the same scope
TaskGroupRun a dynamic number of child tasks in parallelYes, collected as they finish
await (alone)Await a single async call sequentiallyNo, waits before continuing
Note: Check Task.isCancelled inside long-running async work, or call try Task.checkCancellation(), so a cancelled task stops promptly instead of wasting work.
Note: Task { } inherits the actor context it's created in, but the closure itself starts running as new unstructured work — don't assume it finishes, or even starts, before the line right after it executes.

Exercise: Swift Concurrency

What does marking a function async indicate about it?