DEV Community

Vinayak G Hejib
Vinayak G Hejib

Posted on

What Really Happens When You Call an async Function in Swift?

"Calling await in Swift feels like casting a spell — except Swift is the sorcerer and you're just waving a wand.
And like any good spell, there's a secret potion brewing underneath.
We’ve all been there — late-night debugging, coffee in hand, watching your async function freeze and unfreeze like some kind of dark magic!!"

Swift’s async/await model makes asynchronous code elegant, readable, and non-blocking. But if you've ever paused mid-debug and wondered:

“Wait... where did my function actually go after I hit await?”

—you’re in the right place.

This blog post takes you behind the scenes of Swift's async functions, demystifying what really happens when you call them; underneath that calm, elegant surface is a full-blown circus of continuations, task schedulers, and a meticulously crafted state machine — all generated for you by the Swift compiler.

Let us consider as an example:

When you write something like await fetchItems(), it feels like magic. But behind the curtain, Swift is doing a whole lot of heavy lifting to make async/await feel seamless — while keeping your app smooth and responsive. Let’s dive into how async functions really work in Swift, what makes them efficient, and why they’re much more than just syntactic sugar over callbacks.


At first glance, async functions look like ordinary sequential code:

let user = await fetchUser()
let posts = await fetchPosts(for: user)
Enter fullscreen mode Exit fullscreen mode

To you, the developer, it reads top-to-bottom — just like synchronous code.

But in reality, every time you hit an await, Swift pauses the function, hands over control to something else (like the main run loop), and resumes it later when the awaited value becomes available. This is more like a theatrical intermission than a simple pause.


Continuations: The Brain 🧠 Behind await

Swift uses a powerful concept called continuations. Think of a continuation as a "bookmark" for the current point in a function — it stores:

  • The function's local variables,
  • The point where execution left off,
  • And the next instruction to run when the awaited task completes.

So when you write:

let profile = await fetchUserProfile()
Enter fullscreen mode Exit fullscreen mode

Swift compiles this into a state machine that can:

  • Suspend itself,
  • Store context,
  • And resume from exactly where it left off.

In lower-level terms, Swift transforms your async function into something like this:

enum FetchState {
    case start
    case waitingForProfile
    case done
}
Enter fullscreen mode Exit fullscreen mode

This machinery is automatically generated during compilation — so your async function becomes a state machine under the hood, but you don’t have to manage that complexity manually.


Under the Hood: Swift’s Task System

Behind all of this is Swift’s structured concurrency model, built on top of a cooperative task system.

When you create a new async task:

Task {
    await loadData()
}
Enter fullscreen mode Exit fullscreen mode

Swift:

  • Allocates memory for a new task object,
  • Schedules it for execution on a global executor,
  • Executes its body until the first await,
  • Then suspends it — saving its continuation.

Later, when the awaited result is ready, the continuation is resumed on the appropriate executor (often the main actor if you're on the UI thread).

This model is non-blocking, so multiple async tasks can be scheduled and paused concurrently — all without spawning heavyweight threads.


Async/await ≠ Threads

This is worth highlighting: calling an async function does NOT create a new thread.

If you call await fetchData() from the main thread, the function suspends itself without blocking — freeing up the thread to keep UI rendering smooth. When the awaited result is available, Swift resumes the function on the appropriate thread or executor.

This makes async/await an extremely lightweight and efficient model — far better than the GCD + callbacks spaghetti many iOS developers had to deal with in the past.


The Magic of await in SwiftUI

If you’re using SwiftUI and structured concurrency, all of this allows you to write elegant code like:

@MainActor
func load() async {
    self.isLoading = true
    let result = await networkService.fetchData()
    self.data = result
    self.isLoading = false
}
Enter fullscreen mode Exit fullscreen mode

Thanks to the underlying continuation-based machinery, you don’t have to think about:

  • Dispatch queues,
  • Completion handlers,
  • Thread safety.

Swift takes care of it. But knowing how it works under the hood can help you reason about bugs, optimize performance, and understand suspension points better.


Bonus: Checked Continuations

Sometimes you need to bridge old completion-based APIs with Swift concurrency. That’s where checked continuations come in:

func legacyLogin() async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        legacyAuthService.login { result in
            switch result {
            case .success(let user):
                continuation.resume(returning: user)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This allows you to “manually” capture and resume the continuation — but it also includes safety checks to prevent misuse (e.g., multiple resumes, never-resumed bugs).


🧵 Wrapping Up: Async as State Machines

To recap, when you write an async function in Swift:

✅ It’s compiled into a state machine that can suspend and resume.

✅ Suspension happens via continuations stored in memory.

✅ Async functions don’t block threads — they’re suspended cooperatively.

✅ Swift’s structured concurrency handles lifecycle, error propagation, and cancellation for you.

Knowing this helps you appreciate the performance, safety, and elegance Swift offers — and gives you the tools to write more thoughtful concurrent code.

The image visualizes key concepts like:

  • Suspension points as space stations where the function can pause and resume
  • The main thread continuing to handle other tasks (like UI updates)
  • Task bubbles representing concurrent work happening in the background
  • Swift's cooperative nature where async functions play nicely with the system

💡 Final Thought

Next time you type await, remember: you’re not pausing the universe — you’re crafting a finely orchestrated performance of mini-intermissions, backstage crew (executors), and set changes (continuations) that bring modern concurrency to life.

Thanks for reading :)

Top comments (0)