The Problem with withCheckedThrowingContinuation and Task Cancelation: A Video Capturing Example

Ahmed Yamany
4 min readNov 28, 2024

Swift’s withCheckedThrowingContinuation allows you to bridge callback-based APIs into the modern async/await paradigm. While this makes asynchronous code more readable and easier to maintain, it can introduce issues when handling task cancelation. Specifically, canceling a Task using Swift concurrency does not automatically cancel the underlying operation or thread that was initiated via a callback-based API.

This article explores this issue using video capturing as an example and discusses how to handle it correctly with TaskCancellationHandler

Bridging Callbacks to Async with withCheckedThrowingContinuation

To demonstrate, consider a video capturing process. A traditional video capture API might involve starting a recording and stopping it through callback mechanisms:

func startVideoCapture(completion: @escaping (Result<URL, Error>) -> Void) {
// Starts video capturing and asynchronously calls the completion handler with the file URL or an error
}

When converting this into an async function using withCheckedThrowingContinuation, the process might look like this:

func startVideoCaptureAsync() async throws -> URL {
return try await withCheckedThrowingContinuation { continuation in
startVideoCapture { result in
switch result {
case .success(let url):
continuation.resume(returning: url)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

While this works well for making the API awaitable, it does not address cancelation. If a Task running this startVideoCaptureAsync function is canceled, the underlying video capturing operation will continue running, consuming resources unnecessarily. This is because the continuation does not propagate the cancelation signal to the video capture process.

The Real Problem: Task Cancelation Does Not Cancel Underlying Operations

In Swift concurrency, canceling a Task only sets its cancelation flag. If your operation is using a callback-based API bridged through withCheckedThrowingContinuation, the underlying thread or operation does not know about this cancelation unless you explicitly handle it.

This mismatch can lead to:

  1. Wasted Resources: The video capture process continues running even though the result will never be used.
  2. Unpredictable Behavior: The video capture may complete and trigger its callback long after the associated task is canceled, potentially leading to race conditions or memory issues.

How to Solve This

To handle cancelation properly, you must actively stop the underlying operation when a Task is canceled. In the case of video capturing, you need to stop the recording process when cancelation is requested. Swift provides TaskCancellationHandler for this purpose.

Here’s how you can apply it:

  1. Use withTaskCancellationHandler to monitor cancelation signals.
  2. Stop the video capture operation manually if the Task is canceled.
func startVideoCaptureAsync() async throws -> URL {
try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
startVideoCapture { result in
// Check for cancelation before resuming the continuation
guard !Task.isCancelled else {
stopVideoCapture() // Cleanup immediately
continuation.resume(throwing: CancellationError())
return
}

switch result {
case .success(let url):
continuation.resume(returning: url)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}, onCancel: {
stopVideoCapture() // Cleanup when the task is canceled
}
)
}

func stopVideoCapture() {
// Logic to stop video capturing
print("Video capturing stopped.")
}

Why This Works

  • The withTaskCancellationHandler ensures that if the Task running startVideoCaptureAsync is canceled, the onCancel block is executed. This block is responsible for stopping the video capturing process.
  • Inside the operation closure, we check for Task.isCancelled. If the task is canceled, we stop the video capture immediately and throw a CancellationError.
  • By checking for cancelation before resuming the continuation, we prevent race conditions and ensure the continuation is not incorrectly resumed after cancelation.

A Real-World Example: Using a Video Recording Task

Here’s how this function might be used in practice:

func recordVideo() async {
let task = Task {
do {
let videoURL = try await startVideoCaptureAsync()
print("Video captured at \(videoURL)")
} catch is CancellationError {
print("Video recording was canceled.")
} catch {
print("Failed to record video: \(error)")
}
}

// Simulate user canceling the task
await Task.sleep(2 * 1_000_000_000) // Simulate some delay
task.cancel() // Cancel the task after 2 seconds
}

What Happens in This Scenario:

  1. The startVideoCaptureAsync function starts the video recording process.
  2. After 2 seconds, the Task is canceled.
  3. The TaskCancellationHandler runs, calling stopVideoCapture() to terminate the video recording.
  4. The underlying operation stops cleanly, and no resources are wasted.

The Risks of Skipping Cancelation Handling

If you omit withTaskCancellationHandler and rely solely on withCheckedThrowingContinuation, the following problems might occur:

  1. The video recording would continue indefinitely, wasting resources even though the Task is canceled.
  2. The callback could execute much later, potentially resuming a continuation that was already discarded. This might lead to crashes or unexpected behavior.

Handling cancelation ensures clean and predictable behavior in your code.

Conclusion

When converting callback-based APIs into async/await using withCheckedThrowingContinuation, you should handle cancelation. Canceling a Task in Swift concurrency does not automatically propagate to the underlying operation initiated by the callback-based API.

By using withTaskCancellationHandler, you can respond to cancelation signals and stop the operation cleanly. This approach not only avoids wasting resources but also ensures safer and more predictable behavior in your application.

The video capturing example highlights the importance of this practice, but the same principle applies to any long-running operations or APIs you bridge with withCheckedThrowingContinuation. Always make sure to clean up resources when canceling a task!

Big Thanks to Omar Elsayed for his help to understand this concept

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Ahmed Yamany
Ahmed Yamany

Written by Ahmed Yamany

I'm dedicated and passionate, seeking new challenges to extend my expertise

No responses yet

Write a response