What is a Checked Continuation in Swift?
What is a Checked Continuation in Swift?
A checked continuation in Swift is a mechanism provided by Swift Concurrency to bridge closure-based asynchronous code into the structured async/await world. It ensures that an async function can return a value or throw an error only once, helping prevent common concurrency issues like double-resumption or forgetting to resume.
Types of Checked Continuations
Swift provides two types of checked continuations:
1️⃣ withCheckedContinuation
2️⃣ withCheckedThrowingContinuation
The Problem: Callback Hell in Legacy APIs
Imagine this: You’re working on an iOS app that fetches user data from a remote API. But the API you’re dealing with still uses completion handlers. Your code looks something like this:
1
2
3
4
5
6
7
8
9
10
func fetchUserData(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
let success = Bool.random()
if success {
completion(.success("User Data Fetched"))
} else {
completion(.failure(NSError(domain: "FetchError", code: 1)))
}
}
}
Not only does this trap us in callback hell, but error handling gets messier as the project scales.
Wouldn’t it be great if we could use async/await instead?
The Solution: Bridging Callbacks with Checked Continuations
Swift’s withCheckedThrowingContinuation lets us transform legacy completion-based APIs into async/await with just a few lines of code.
Here’s how we can rewrite fetchUserData() using checked continuations:
1
2
3
4
5
6
7
8
9
10
11
12
func fetchUserData() async throws -> String {
return try await withCheckedThrowingContinuation { continuation in
fetchUserData { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
Now, when we call this function, it looks cleaner and more readable:
1
2
3
4
5
6
7
8
Task {
do {
let userData = try await fetchUserData()
print("✅ Success:", userData)
} catch {
print("❌ Error:", error)
}
}
✅ No more nested callbacks.
✅ No more state confusion.
✅ Just clean, structured concurrency!
Real-World Use Case: API Calls, Database Fetching, and More
Checked continuations are a lifesaver when working with:
- ✅ Networking APIs (
URLSession,Firebase,Alamofire) - ✅ Database queries (
CoreData,Realm,SQLite) - ✅ Bluetooth communication (
CoreBluetooth) - ✅ Third-party SDKs with legacy callbacks
For example, in CoreData:
1
2
3
4
5
6
7
8
9
10
func fetchCoreDataEntity() async throws -> [UserEntity] {
return try await withCheckedThrowingContinuation { continuation in
do {
let users = try context.fetch(UserEntity.fetchRequest())
continuation.resume(returning: users)
} catch {
continuation.resume(throwing: error)
}
}
}
This lets you write code that’s safer, easier to debug, and scales well with Swift’s structured concurrency.
✅ Key Takeaways
1️⃣ Checked Continuations prevent double resumption
→ No more accidental crashes!
2️⃣ They bridge old APIs to async/await
→ Making Swift code more readable and modern.
3️⃣ Debugging is safer
→ Swift will warn you if you forget to resume a continuation.
If you’re still using completion handlers in your iOS app, it’s time to upgrade to async/await with checked continuations.
🚀 Have you used withCheckedThrowingContinuation before?
What challenges did you face when migrating to async/await?
🎉 Happy Swifting! 🚀
Follow me in LinkedIn for more informative posts.
