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.
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.