Published on

Update Your Nested Completion Handlers To Async Await In Swift

Authors
  • avatar
    Name
    Kiet
    Twitter

When you're working with Swift right now, in 2023, there are a bunch of libraries that haven’t yet supported the async await API. So you need to use the completion handler for that API.

But Apple also introduced a way to convert that old API to the latest async await API, called Continuation. In this article, I will give a small example of how to use the continuation API.

“A continuation is a mechanism to interface between synchronous and asynchronous code.”

Why do I need to use async await?

Because async await is the revolution model in Swift concurrency, instead of having multiple completion handlers nested with each other, we write code just like synchronous code. That makes the code easy to read and write.

Example: Fetch data from a URL

Using the completion handler:

func fetchData() {
  URLSession.shared.dataTask(
    with: URL(string: "https://cthe.dev")!) { data, response, error in
      // your completion handler code
    }
}

This code is very simple because we only have 1 completion handler here, but just imagine you need to pass a completion handler to another completion handler, and so on and so on, …

Fortunately, Apple already gives us an async await version of URLSession.shared.data(from: URL) API, so we can easily use it like below.

func fetchData() async throws -> Data {
  let (data, _) = try await URLSession.shared.data(
    from: URL(string: "https://cthe.dev")!
  )
  return data
}

So you love the simplicity of async await, so let's try to use Continuation to convert URLSession.shared.dataTask to the async await API. 😍

Note that we just use this as an example to study how to use Continuation. We don’t need to do this in the real world because there is already URLSession.shared.data(from: URL) provided by Apple.

Use async await with CheckContinuation:

func fetchData() async throws -> Data {
  return try await withCheckedThrowingContinuation { continuation in
    URLSession.shared.dataTask(
      with: URL(string: "https://cthe.dev")!) { data, _, error in
        if let error = error {
          continuation.resume(throwing: error)
         } else if let data = data {
            continuation.resume(returning: data)
         } else {
           continuation.resume(throwing: URLError(.unknown))
         }
      }
  }
}

When using the above withCheckedThrowingContinuation function, the current task will be suspended, and the code in the given closure will execute until resumed with continuation.resume, so we need to make sure we call continuation.resume exactly ONE TIME. If the continuation has already resumed, then calling this method results in undefined behavior.

Credits

How to use Continuations in Swift by Swiftful Thinking