Swift Continuations

Mar 10, 2023#swift#concurrency

Swift continuations —CheckedContinuation and UnsafeContinuation— are opaque representations of program state used to bridge between synchronous and asynchronous code. Swift assigns each asynchronous unit of work a continuation instead of creating an entire thread for it.

Swift provides two almost-identical set of continuation APIs, unsafe vs checked. Because both types have the same interface, you can replace one with the other in most circumstances, without making other changes. To create a continuation in asynchronous code, call one of following functions:

Asynchronous tasks can suspend themselves on continuations which synchronous code (completion callbacks or delegate methods) can then capture and invoke to resume the task in response to an event. You must call one of following resume methods exactly once on every execution path throughout the program, resuming from a continuation more than once is undefined behavior.

  • resume(returning:)
  • resume(throwing:)
  • resume(with:)
  • resume()

Given a completion callback-based function, we can turn it into an async function by suspending the task and using its continuation to resume it when the callback is invoked, turning the argument passed into the callback into the normal return value of the async function:

// Callback-based function
func fetchData(_ completionHandler: @escaping (Result<Data, Error>) -> Void) {
  //
}

// Using withCheckedContinuation 
func fetchData() async -> Result<Data, Error> {
  return await withCheckedContinuation { continuation in
    fetchData { result in
      continuation.resume(returning: result)
    }
  }
}

// Using withCheckedThrowingContinuation
func fetchData() async throws -> Data {
  return try await withCheckedThrowingContinuation { continuation in
    fetchData { result in
      switch result {
      case .success(let value):
        continuation.resume(returning: value)
      case .failure(let error):
        continuation.resume(throwing: error)
      }
    }
  }
}

Resuming from a continuation more than once is undefined behavior. Never resuming leaves the task in a suspended state indefinitely, and leaks any associated resources.

Unsafe continuations

Unsafe continuations avoid enforcing correctness violations, resuming multiple times or never resuming, at runtime because they aim to be a low-overhead mechanism for interfacing tasks with event loops, delegate methods, callbacks, and other non-async scheduling mechanisms.

struct UnsafeContinuation<T, E: Error> {
  func resume(returning: T)
  func resume(throwing: E)
  func resume(with result: Result<T, E>)
}

extension UnsafeContinuation where T == Void {
  func resume() { resume(returning: ()) }
}

extension UnsafeContinuation where E == Error {
  func resume<ResultError: Error>(with result: Result<T, ResultError>)
}

func withUnsafeContinuation<T>(
  _ operation: (UnsafeContinuation<T, Never>) -> ()
) async -> T

func withUnsafeThrowingContinuation<T>(
  _ operation: (UnsafeContinuation<T, Error>) throws -> ()
) async throws -> T

Checked continuations

Checked continuations perform runtime checks for missing or multiple resume operations, and log a message if either of these invariants is violated. They are intentionally identical to the Unsafe variants, so that you can switch easily between the checked and unchecked.

struct CheckedContinuation<T, E: Error> {
  func resume(returning: T)
  func resume(throwing: E)
  func resume(with result: Result<T, E>)
}

extension CheckedContinuation where T == Void {
  func resume()
}

extension CheckedContinuation where E == Error {
  func resume<ResultError: Error>(with result: Result<T, ResultError>)
}

func withCheckedContinuation<T>(
  _ operation: (CheckedContinuation<T, Never>) -> ()
) async -> T

func withCheckedThrowingContinuation<T>(
  _ operation: (CheckedContinuation<T, Error>) throws -> ()
) async throws -> T