Continuations in Swift Concurrency
Bridge callback-based APIs to async/await and understand the difference between checked and unsafe continuations.
Read the Korean original on Velog ↗English translation

A completion-handler API can interrupt the otherwise straightforward flow of async/await code. Continuations bridge that gap, but they also introduce an important responsibility: signal completion exactly once.
From completion handlers to async/await
Continuations adapt older asynchronous patterns, such as completion handlers, for use with async/await. This makes the surrounding code easier to follow and lets error handling use the same structure as other asynchronous calls.
// Existing completion-handler API
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
// Handle a successful response
} else if let error = error {
// Handle a request error
} else {
// Handle other cases
}
}
.resume()
Checked and unsafe continuations
Both APIs suspend the current task until the continuation is resumed. Their main difference is the runtime checking they perform. Use the throwing variants when the operation can fail.
withCheckedContinuation
The checked version is usually the best starting point. It detects incorrect use, including attempts to resume a continuation more than once.
withUnsafeContinuation
The unsafe version omits those checks. This can reduce overhead, but incorrect use is harder to diagnose and can lead to undefined behavior.
For throwing operations, use
withCheckedThrowingContinuationorwithUnsafeThrowingContinuation.
Resuming a continuation
A continuation's resume methods signal completion and deliver a result or an error.
resume(returning:)
Resumes a successful operation with its result.
resume(throwing:)
Resumes a failed operation by throwing the supplied error.
Every continuation must be resumed exactly once across all execution paths.
class ContinuationNetworkManager {
func getDataAsync(url: URL) async throws -> Data {
// Bridge to async/await
return try await withCheckedThrowingContinuation { continuation in
// Existing asynchronous implementation
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
// Return the result
continuation.resume(returning: data)
} else if let error = error {
// Return the error
continuation.resume(throwing: error)
} else {
// Return the error
continuation.resume(throwing: URLError(.badURL))
}
}
.resume()
}
}
}
class ContinuationViewModel: ObservableObject {
@Published var image: UIImage? = nil
let networkManager = ContinuationNetworkManager()
func getImage() async {
guard let url = URL(string: "https://picsum.photos/200") else { return }
do {
// Call with async/await
let data = try await networkManager.getDataAsync(url: url)
if let image = UIImage(data: data) {
await MainActor.run(body: {
self.image = image
})
}
} catch {
print(error)
}
}
}