Sitemap
Adarsh Ranjan

Adarsh Ranjan

What Really Happens When Swift Sees await

5 min readJul 25, 2026

--

Part 1: The Mental Model That Changes Everything The Biggest Misconception in Swift Concurrency

Ask any iOS developer what await does, and you'll likely hear:

“It runs code on a background thread.”

This is wrong. It’s not just slightly inaccurate — it fundamentally misunderstands how Swift concurrency works. And this misconception leads to subtle bugs, performance issues, and confusion that can take months to unravel.

Here’s the real statement:

await does not switch threads. It marks a potential suspension point.

If you remember only one thing from this article, remember that. await is about suspension, not thread switching.

What is a Task? (And Why It’s Not a Thread)

Before we can understand await, we need to understand the hierarchy of execution in Swift:

Process → Thread → Task

  • Process: A running program with its own memory space (e.g., your iOS app)
  • Thread: An operating system concept — a path of execution managed by the kernel. Threads are expensive (typically ~1MB of stack space each)
  • Task: Swift’s concurrency primitive — a lightweight unit of work. Tasks are not threads

Here’s the critical relationship:

Threads execute tasks. A thread can execute many tasks over its lifetime, but only one task at a time.

Think of it like this:

  • Thread = A worker
  • Task = A project
  • A worker can work on one project at a time
  • When a project pauses (suspends), the worker can start another project
  • The project may later resume with a different worker (different thread)

This is the foundation of Swift’s concurrency model: many tasks, fewer threads.

The Anatomy of an await

Let’s look at the simplest possible async function:

func work() async {
print("A")

let users = await fetchUsers()

print("B")
}

When Swift encounters this code, something remarkable happens. Let’s trace it step by step:

Task starts
↓
print("A") ← Executed on whatever thread the task is running on
↓
Hit await ← Potential suspension point detected
↓
Can fetchUsers() complete immediately?
↓
YES (cached result) → Continue without suspending
↓
NO (needs network) → SUSPEND THE TASK
↓
Save task state
↓
Release the thread
↓
Wait for network operation to complete
↓
Operation completes
↓
Scheduler chooses an executor
↓
Resume task on ANY available thread
↓
print("B") ← May run on a completely different thread!

This is the heart of Swift concurrency.

Notice what happened:

  1. The thread was released during suspension
  2. The task did not block the thread
  3. The thread could work on other tasks while waiting
  4. The task resumed on any available thread

This is why await is so efficient—it allows massive concurrency without creating thousands of threads.

What Actually Gets Saved During Suspension?

When a task suspends, Swift must preserve everything needed to resume later. This is called a continuation:

func processOrder() async {
let userID = 123 // ← Local variable
let discount = 0.10 // ← Local variable
let items = ["book", "pen"] // ← Local variable

let result = await api.placeOrder(items) // ← Suspension point

print("Order placed for user \(userID)") // ← Resume point
}

During suspension, Swift saves:

  1. Local variables: userID, discount, items (their current values)
  2. Resume point: “After line 10, before the print statement”
  3. Stack frame: The function’s execution context
  4. The continuation: An object representing “work to continue”

The continuation is like a bookmark in a book — it remembers exactly where you stopped and everything you need to continue reading.

Task State During Suspension:
┌─────────────────────────────┐
│ Continuation │
│ ├─ Function: processOrder() │
│ ├─ Resume Line: 10 │
│ ├─ Variables: │
│ │ ├─ userID: 123 │
│ │ ├─ discount: 0.10 │
│ │ └─ items: ["book","pen"] │
│ └─ Stack Pointer: 0x7f... │
└─────────────────────────────┘

The Surprising Truth: await Doesn't Always Suspend

Here’s something that surprises most developers:

func hello() async {
print("Hello")
}

await hello() // Does this suspend?

Answer: No.

Get Adarsh Ranjan’s stories in your inbox

Join Medium for free to get updates from this writer.

Let’s trace it:

await hello()
↓
Can hello() complete immediately?
↓
YES (it just prints, no async work inside)
↓
No suspension
↓
Continue execution

This leads to a crucial insight:

await marks a potential suspension point, not a guaranteed one.

await is a suspension point marker, not a suspension command. It tells the compiler: "At this point, this task might suspend. Be prepared."

Ghost Await: The Most Confusing Case

Now we come to one of the most misunderstood aspects of Swift concurrency:

func calculate() async -> Int {
42 // ← No async work, no suspension
}

let result = await calculate() // ← Why do we need await?

This function doesn’t do anything async. There’s no:

  • Network request
  • Actor hop
  • Sleep
  • I/O

Yet we still write await. Why?

Because calculate() is declared async. Its contract says "this function may suspend."

Swift cannot assume future implementations won’t suspend:

// Current implementation - no suspension
func calculate() async -> Int {
42
}

// Future implementation - could suspend!
func calculate() async -> Int {
try await api.fetchFromServer() // ← Now it suspends
}

The await keyword is part of the function's interface contract, not its implementation. It tells callers: "This function might suspend, so you need to be ready for that."

This is why you see await even when no suspension happens—it's not about what the function does now, but what it might do in the future.

The Runtime Machinery

When your task actually suspends, here’s what happens at the system level:

Your Task
↓
await
↓
Swift Runtime
↓
Check if work can complete immediately
↓
NO → Create continuation
↓
Call suspendAsync() on task
↓
Save task state
↓
Return thread to thread pool
↓
[Thread is free to work on other tasks]
↓
[Async operation completes]
↓
Async completion callback fires
↓
Resume continuation on available thread
↓
Restore task state
↓
Continue execution

Visual Summary: The Complete await Flow

┌─────────────────────────────────────────────────────────────────┐
│ TASK EXECUTION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Thread 1] ──► Task starts │
│ │ │
│ ├─► print("A") │
│ │ │
│ ├─► await fetchUsers() ◄─────┐ │
│ │ │ │ │
│ │ ├─► Can complete now? │ │
│ │ │ │ │
│ │ ├─► YES → Continue ───┘ │
│ │ │ │ │
│ │ └─► NO → Suspend │ │
│ │ │ │ │
│ │ ├─► Save │ │
│ │ │ state │ │
│ │ │ │ │
│ │ ├─► Release │ │
│ │ │ Thread 1 │ │
│ │ │ │ │
│ [Thread 1] ──► Task B starts │ │ │
│ │ │ │ │
│ ├─► ... │ │ │
│ │ │ │ │
│ [Network completes] │ │ │
│ │ │ │ │
│ │ ├─► Scheduler│ │
│ │ │ chooses │ │
│ │ │ thread │ │
│ │ │ │ │
│ [Thread 3] ──► Resume Task │ │ │
│ │ │ │ │
│ ├─► Restore ◄─────────────┘ │
│ │ state │
│ │ │
│ ├─► print("B") │
│ │ │
│ └─► Complete │
│ │
└─────────────────────────────────────────────────────────────────┘

What’s Next?

Now that you understand what await actually does, you're ready for the next question:

Where does your code actually run after await?

In Part 2, we’ll explore:

  • How executors determine where tasks resume
  • Why @MainActor ensures UI updates on the main thread
  • The real architecture of URLSession
  • Why network calls don’t block threads

Next in the series: “Where Does Your Async Code Actually Run?” — We’ll dive into executors, MainActor, and the networking stack.

Did this article change your understanding of await? Share what surprised you most in the comments!

[Subscribe for Part 2: URLSession, Threads & Executors]

--

--