withCheckedThrowingContinuation: Bridge Callbacks to Async/Await
withCheckedThrowingContinuation bridges legacy completion-handler APIs into async/await. Wrap one-shot callback-based work so callers use try await instead of nested blocks. The footgun is resuming twice or never, which checked runtimes catch in debug builds.
Why it exists
Before Swift introduced native concurrency, asynchronous work was expressed through completion handlers and delegate methods. When async/await arrived, there was a massive gap because thousands of APIs already shipped with callback-based signatures that could not be awaited. withCheckedThrowingContinuation was built to close that gap mechanically, letting developers suspend an async task and resume it later from a legacy callback without rewriting the underlying framework.
The mental model
Treat the function like a single-use promise token. Your async code pauses and hands a token to an old-fashioned callback API. When that API finishes, it redeems the token exactly once, either with a successful value or a thrown error, which wakes up the awaiting caller. The checked flavor adds a safety guard. If you accidentally redeem the token twice or let it fall on the floor, Swift raises a runtime warning or fatal error in debug builds, turning a subtle concurrency bug into an obvious crash.
How it works
Inside an async throws function, you call withCheckedThrowingContinuation and receive a CheckedContinuation value in a closure. You start your legacy asynchronous operation from within that closure. In the operation's completion handler, you call continuation.resume(returning:) to deliver a value, or continuation.resume(throwing:) to deliver an error. The runtime enforces that resume happens exactly once. If you call it a second time, the program traps in debug. If the closure returns without resuming, the runtime detects the leaked continuation and logs a warning.
When to use it
Reach for this wrapper when you need to expose a one-shot callback-based API as an async throws function. Typical cases include wrapping Objective-C framework methods, older Swift libraries that use completion handlers, or C APIs that signal completion through function pointers. It shines when the result is a single value or error rather than a sequence.
When not to use it
Do not use it for multi-value streams because AsyncStream and AsyncThrowingStream are the proper tools for callbacks that fire repeatedly. Avoid it when a native async alternative already exists, because the native path avoids the manual resume bookkeeping entirely. Also skip it if the underlying API has complex ordering or stateful delegate callbacks that are easier to reason about in their original form.
One canonical example
Imagine wrapping URLSession.dataTask before the async data(from:) method existed. You enter withCheckedThrowingContinuation, create a data task, and inside the task's completion handler you call resume(returning: data) on success or resume(throwing: networkError) on failure. The caller then writes let (data, response) = try await myWrappedRequest(), getting a flat sequential flow instead of a nested closure pyramid.
Interview question
Which scenario is the best fit for withCheckedThrowingContinuation?
- a.A timer that fires every second to update a counter
- b.A network request that delivers a single response or error via a completion handlerCorrect
- c.A delegate protocol that emits multiple ordered events during a user session
- d.A modern Swift API already exposing native async/await methods
Why? this is the answer
withCheckedThrowingContinuation is designed for one-shot callbacks that resume exactly once with a value or error. A delegate protocol emitting multiple ordered events is tempting to wrap but is explicitly discouraged, as complex stateful delegates are harder to reason about when flattened into a single continuation.
Just read this? Test yourself on what you have been reading.
- #swift
- #concurrency
- #async-await
- #interoperability
- #ios
Put your scrolling time to good use
Learn one idea, try a quiz and save useful cards for revision. Tezvyn makes it easy to learn and stay current in your tech field, a few minutes at a time.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Open roles that interview on swift — each one lists the topics its interview covers.
See open roles