Skip to content
SwiftMind

async / await

Writing code that waits without blocking, and reads top to bottom.

IntermediateNot started9 min
On this page

async marks a function that may suspend. await marks the point where it might.

Suspending is not blocking. A blocked thread sits there occupying a resource while it waits. A suspended function *gives its thread back*: the runtime saves where it was, lets the thread pick up other work, and resumes the function later — possibly on a different thread entirely.

The practical win is that asynchronous code reads like synchronous code. No completion handler nesting, try/catch works normally, and a loop with an await in it is just a loop rather than a recursive callback.

async let starts both immediately; sequential awaits would run them one after the other.
// Before: nesting, and errors handled by handfunc loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {    fetchUser { result in        switch result {        case .success(let user):            fetchAvatar(user.id) { avatarResult in                // ...and so on            }        case .failure(let error): completion(.failure(error))        }    }}
// After: sequential, with ordinary error handlingfunc loadProfile() async throws -> Profile {    let user   = try await fetchUser()    let avatar = try await fetchAvatar(user.id)    return Profile(user: user, avatar: avatar)}
// Independent work should actually overlapfunc loadProfileConcurrently() async throws -> Profile {    async let user   = fetchUser()    async let banner = fetchBanner()    return try await Profile(user: user, banner: banner)}
Show all 26 lines
Suspension gives the thread back
Function startson thread A
awaitsuspension point
Other workthread A is reused
Resumesmaybe thread B
Returns

An async function runs on a thread, suspends at an await, releasing the thread to do other work, then resumes later — possibly on a different thread.

The function begins on some thread.

Step 1 of 3
  1. The function begins on some thread.
  2. At the await it suspends and hands the thread back. Nothing is blocked.
  3. When the awaited work finishes, the function resumes — not necessarily on the same thread.

Under the hood

The compiler splits an async function at every suspension point into a chain of partial functions, and its local state moves off the stack into a heap-allocated async frame. Resuming means invoking the next partial function with that frame. This is why async functions can suspend at all: there's no stack frame to preserve.

Those functions run on a *cooperative thread pool* sized to the number of CPU cores. It deliberately doesn't grow, which is what prevents the thread explosion GCD was prone to — dozens of blocked queues each spawning a thread. The trade is that you must never block a thread in that pool: a sleep, a semaphore wait, or synchronous file I/O inside an async function starves a core, and enough of them can deadlock the pool.

await is also the *only* place a function can suspend. That's a strong guarantee: between two awaits your code runs without interruption on its isolation domain, so you know exactly where state can change beneath you.

Quick check

What does await actually do to the current thread?

  • Blocks it until the result is ready
  • Suspends the function and releases the thread to run other work, resuming later — possibly on a different thread
  • Moves the work to a background thread
  • Nothing; it's a compile-time annotation only
Reveal answer

Suspends the function and releases the thread to run other work, resuming later — possibly on a different thread

Suspension is the whole point. The function's state is saved to a heap frame and the thread returns to the cooperative pool, so a handful of threads can serve thousands of in-flight async operations. Because the thread is released, resumption can happen on a different one — which is why you can't assume thread-local state survives an await.

Interview question

What's the difference between blocking and suspending, and why does it matter?

Think first, then reveal

A blocked thread is still occupied — it's sitting in a wait state, consuming a thread from a finite pool, and it can't do anything else until whatever it's waiting on completes. Suspension releases the thread entirely: the function's state is saved into a heap-allocated frame, the thread goes back to the pool for other work, and the function is resumed later.

It matters because threads are expensive and few. Swift's cooperative pool is sized to the core count, deliberately, to avoid the thread explosion that GCD allowed — where each blocked queue could spawn another thread and a busy app ended up with hundreds, each with its own stack, thrashing the scheduler. With suspension, a handful of threads can serve thousands of concurrent operations, because waiting costs a heap frame rather than a thread.

The practical consequence is the rule that you must not block inside async code. Thread.sleep, a semaphore wait, synchronous file or network I/O — each of those occupies a pool thread that the runtime was counting on. Do it on enough of them and you deadlock: every thread is blocked waiting for work that needs a thread to make progress.

The other consequence is that you can't assume you resume on the thread you left, so thread-local state and "I know I'm on the main thread here" reasoning don't survive an await. Isolation, not threads, is how you express that now.

What an interviewer listens for

  • Distinguishes occupying a thread from releasing it
  • Explains the finite cooperative pool and why it's capped
  • Names concrete blocking hazards and the deadlock they cause
  • Notes you may resume on a different thread

Key takeaway

await suspends rather than blocks — the thread goes back to the pool and your function resumes later, possibly elsewhere. Sequential awaits mean sequential work, so use async let or a task group when the work is independent.

Read these first

Related

0%
Quiz this concept