How to wrap cancellable completion handler function into async code

SE-0300 proposed the following for wrapping cancellable completion handler-based functions into async functions:

func download(url: URL) async throws -> Data? {

    var urlSessionTask: URLSessionTask?

    return try await withTaskCancellationHandler {
        urlSessionTask?.cancel()
    } operation: {
        let result: Data? = try await withUnsafeThrowingContinuation { continuation in
            urlSessionTask = URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            }
            urlSessionTask?.resume()
        }
        if let result = result {
            return result
        } else {
            return nil
        }
    }
}

However, that no longer works, as the compiler gives the following error:

Reference to captured var 'urlSessionTask' in concurrently-executing code

What is the up-to-date best practice on wrapping such code in a way that cancellations are properly propagated to underlying tasks?

The original pitch had a really nice suggestion for the API, but it doesn't seem like anything like that exists.

AsyncStream has a Continuation type where one can register onTermination, but the continuation given by withChecked|Unsafe(Throwing)Continuation does not have it.

This exact example is being discussed in this thread: How to use withTaskCancellationHandler properly? - #12 by gabeshahbazian

2 Likes