How to bridge completions handlers to Swift's async/await
Understand Swift continuations to create your own async/await functions.
05 Dec 2022 · 3 min read
Before Apple introduced Swift's new async/await concurrency model at WWDC21, completion callbacks were a common pattern to work with asynchronous code.
With completion callbacks, we pass in a closure that executes when the work completes. When adapting iOS applications to async/await, we need a way to bridge completion handlers to async functions. This is where so called continuations come in.

Every time we call await, the current code suspends by creating a continuation that captures the state at the point of suspension. When an awaited function completes, the captured state is recreated from the continuation and the original code resumes. This all happens behind the scenes when we use existing async functions.
To create our own async functions, we can use so called continuation functions. Continuation functions give us control over suspending and resuming a function.
Let's see how to do that.
Using withCheckedContinuation()
Let's jump into a small example and create an async function version from the following completion handler function:
func load(completion: (Int) -> Void) {...}
The async version of that function can be created as follows:
func load() async -> Int {return await withCheckedContinuation({ continuation inload() { result incontinuation.resume(returning: result)}})}
In the example above, we use Swift's withCheckedContinuation function to suspend and then call resume after we get a result from our completion handler function. Resuming from a continuation must happen exactly once.
Besides withCheckedContinuation(), there are other continuation functions we can use:
Using withCheckedThrowingContinuation()
The withCheckedThrowingContinuation() function behaves the same as withCheckedContinuation(), additionally allowing to handle errors. For example, if our completion handler function completes with a possible error Result<Int, Error>:
func load(completion: (Result<Int, Error>) -> Void) {}
We could wrap it into an async throws function as follows:
func load() async throws -> Int {return try await withCheckedThrowingContinuation({ continuation inload() { result inswitch result {case .success(let successResult):continuation.resume(returning: successResult)case .failure(let error):continuation.resume(throwing: error)}}})}
Using withUnsafeContinuation() and withUnsafeThrowingContinuation()
Both functions, withUnsafeContinuation() and withUnsafeThrowingContinuation() behave the same as withCheckedContinuation() and withCheckedThrowingContinuation(). The only difference is that while checked continuations perform runtime checks for missing or multiple resume operations, unsafe continuations don't.
For development, checked continuations are a good choice because they make testing easier providing direct feedback for missing or multiple resume operations. Since both types have the same interface, we can replace them with unsafe continuations later on without making any other changes to reduce the overhead.

Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips