Hey Swifters :), In this guide i would like to talk about continuations in Swift and their types. Let’s say you are working on your iOS app, and you need to use an old networking library that still uses completion handlers. But your shiny new Swift code is all async/await. It’s like trying to connect a vintage radio to a modern Bluetooth speaker. This is where Swift continuations come to the rescue. Think of them as translators that help your old callback based code talk to your new async/await code. Let’s start with
What Are Continuations Anyway?
Before we jump into the different types, let’s understand what a continuation actually is. Imagine you are reading a book and someone interrupts you, you put a bookmark on the page so you can continue reading later. A continuation in Swift is basically that bookmark, it remembers where your async function left off so it can resume later when the callback fires. Swift gives us four different continuation functions, each with its own personality:
withUnsafeContinuation
withCheckedContinuation
withUnsafeThrowingContinuation
withCheckedThrowingContinuation
Lets explore them one by one.
1. withUnsafeContinuation:
This is the Formula 1 car of continuations, fast but dangerous if you don’t know what you’re doing. Letss say you have an old school timer function that uses callbacks:
func oldSchoolTimer(seconds: Int, completion: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds)) {
completion()
}
}
Before (Callback Hell):
func doSomethingWithTimer() {
oldSchoolTimer(seconds: 2) {
print("Timer finished!")
// Now what if you need another timer?
oldSchoolTimer(seconds: 1) {
print("Second timer finished!")
// And another? This gets messy fast...
}
}
}
After (Async Paradise):
func modernTimer(seconds: Int) async {
await withUnsafeContinuation { continuation in
oldSchoolTimer(seconds: seconds) {
continuation.resume() // This is our "bookmark" being used
}
}
}
func doSomethingWithTimer() async {
await modernTimer(seconds: 2)
print("Timer finished!")
await modernTimer(seconds: 1)
print("Second timer finished!")
// Much cleaner, right?
}
Why “Unsafe”? It’s called “unsafe” because Swift won’t check if you’re using the continuation correctly. If you call continuation.resume()
twice, or never call it at all, your app might crash or hang forever. The key rules for Unsafe Continuations are, call resume()
exactly once, call it from any thread (it’s thread-safe) and don’t store the continuation and use it later.
2. withCheckedContinuation:
This is the same as withUnsafeContinuation
, but with training wheels. Swift keeps an eye on you and will crash your app with a helpful error message if you mess up during development.
func safeModernTimer(seconds: Int) async {
await withCheckedContinuation { continuation in
oldSchoolTimer(seconds: seconds) {
continuation.resume()
// If you accidentally call resume() again here,
// Swift will catch it in debug mode!
}
}
}
// if called twice
// Error: _Concurrency/CheckedContinuation.swift:169: Fatal error: SWIFT TASK CONTINUATION MISUSE: safeModernTimer(seconds:) tried to resume its continuation more than once, returning ()!
Why Use Checked? Because it catches mistakes during development, is slightly slower than unsafe (but negligible) and is perfect for learning and debugging. When to Use Which? well use withCheckedContinuation
while developing and testing and switch to withUnsafeContinuation
for production if you need every bit of performance.
3. withUnsafeThrowingContinuation:
Now things get interesting. What if your callback based function can fail? This continuation can handle errors too. Lets say you have a network request function that can fail
func fetchUserData(userID: String, completion: @escaping (Result<User, Error>) -> Void) {
// Simulating a network request
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
if userID == "invalid" {
completion(.failure(NetworkError.invalidUserID))
} else {
completion(.success(User(name: "John Doe")))
}
}
}
enum NetworkError: Error {
case invalidUserID
}
struct User {
let name: String
}
Before (Callback with Result):
func getUserInfo() {
fetchUserData(userID: "123") { result in
switch result {
case .success(let user):
print("Got user: \(user.name)")
case .failure(let error):
print("Error: \(error)")
}
}
}
After (Async with Throws):
func modernFetchUser(userID: String) async throws -> User {
return try await withUnsafeThrowingContinuation { continuation in
fetchUserData(userID: userID) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
func getUserInfo() async {
do {
let user = try await modernFetchUser(userID: "123")
print("Got user: \(user.name)")
} catch {
print("Error: \(error)")
}
}
With this you get clean error handling with do-catch, no more nested Result handling and errors bubble up naturally through the async chain.
4. withCheckedThrowingContinuation:
This is the safety conscious version of the throwing continuation. Its like withUnsafeThrowingContinuation
but with safety checks enabled.
func safeModernFetchUser(userID: String) async throws -> User {
return try await withCheckedThrowingContinuation { continuation in
fetchUserData(userID: userID) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
// Swift will catch it if you try to resume again!
}
}
}
}
What about the Isolation Parameter:
You might have noticed withUnsafeThrowingContinuation(isolation:_:)
in the topic. The isolation
parameter is a newer addition that helps with actor isolation. Here is what it means:
@MainActor
class ViewController {
let label: UILabel = {
let label = UILabel()
return label
}()
func updateUI() async {
let data = try? await withUnsafeThrowingContinuation(isolation: MainActor.shared) { continuation in
// This closure runs isolated to the MainActor
fetchUserData(userID: "user_id_here") { result in
continuation.resume(returning: result)
}
}
// We're guaranteed to be back on MainActor here
self.label.text = "Some text"
}
}
Continuations are pretty handy when it comes to dealing with old implementations that use callbacks. If you want learn about it, check out the official documentation at: https://developer.apple.com/documentation/swift/withunsafethrowingcontinuation(isolation:_:)
Arlight, so that was all about continuations in Swift. I hope you enjoyed this one and i will see you in the next one. Happy coding. 🙂