Swift how to wrap completion into an async/await?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When you have an older Swift API that uses a completion handler, the usual way to expose it as async and await is to wrap it with a continuation. Swift provides withCheckedContinuation for non-throwing results and withCheckedThrowingContinuation for throwing ones. The key rule is simple: resume the continuation exactly once.
Wrap a Simple Completion Handler
Suppose you start with an old-style API:
You can expose it as async like this:
Now callers can write:
This is the standard migration pattern for non-throwing completion callbacks.
Wrap a Throwing Completion Handler
If the original API returns a result-or-error pair, use withCheckedThrowingContinuation.
And then call it like:
This is usually the cleanest way to bridge callback-based APIs into Swift concurrency.
Choose the Right Continuation Type
Use withCheckedContinuation when:
- the completion never reports an error
- the async function should not throw
Use withCheckedThrowingContinuation when:
- the completion can fail
- the async wrapper should propagate errors
The checked variants are usually preferable because Swift can diagnose misuse during development more helpfully than the unsafe variants.
Resume Exactly Once
This is the most important correctness rule. A continuation must be resumed once and only once.
Wrong patterns include:
- forgetting to resume on an error path
- resuming twice because multiple callbacks fire
- returning before the callback ever happens
If the wrapped API is buggy or can call its completion multiple times, you need extra protection before bridging it into async and await.
Cancellation Is Not Automatic
Wrapping a completion handler does not automatically make the underlying work cancellable. If the old API supports cancellation, you need to model that explicitly.
For example, an async wrapper may still wait for work that continues in the background even after the task is cancelled. Bridging callback code into Swift concurrency improves the call site, but it does not magically redesign the underlying API.
That distinction matters in network and long-running operations.
Common Pitfalls
The biggest mistake is resuming the continuation more than once. Swift concurrency assumes the bridge is well behaved, and double resume is a serious bug.
Another common issue is choosing the non-throwing continuation for an API that can fail. If errors exist, model them as throws.
Developers also sometimes wrap a callback that can never fire under some conditions. That leaves the awaiting task suspended forever.
Finally, do not confuse "wrapped in async" with "now cancellation-safe and structured." The wrapper improves ergonomics, but cancellation and resource cleanup still need deliberate design.
Summary
- Wrap callback-based APIs with
withCheckedContinuationorwithCheckedThrowingContinuation. - Use the throwing form when the original completion can fail.
- Resume the continuation exactly once.
- Expose the wrapped function as a normal
asyncorasync throwsAPI. - Remember that cancellation behavior does not become correct automatically just because the call site now uses
await.

