← Back to archive

Development · Concurrency

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

Development archive cover

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 withCheckedThrowingContinuation or withUnsafeThrowingContinuation.

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)
        }
    }
}
← Back to archive