I want to create a URL request and pass it into an async let binding, which seems natural to me:
func test() async {
// Force unwraps (!) are just for demo
var request = URLRequest(url: URL(string:"https://stackoverflow.com")!)
request.httpMethod = "GET" // just for example
// some more tinkering with `request` here.
// 🔴 Error on this line: "Reference to captured var 'request' in concurrently-executing code"
async let responseData = URLSession.shared.data(for: request).0
// It works like this:
// let immutableRequest = request
// async let responseData = URLSession.shared.data(for: immutableRequest).0
// other stuff
print("Response body: \(String(data: try! await responseData, encoding: .utf8))")
}
Why do I get an error? URLRequest
is a struct, so when we pass it into a function, the function should get a copy of that struct, so if I modify request
after the async call, it shouldn't affect the call.
I know that the call happens asynchronously, but I would expect it to capture the parameters at the point of the call and then continue execution as though the call has been made (so, a copy of request
at the point of the call has been passed into data(for: request)
.
Also, is there a convenient way to do it without creating another let
variable and without using a closure to initialize request
, like:
let request: URLRequest = {
var result = URLRequest(url: URL(string:"https://stackoverflow.com")!)
result.httpMethod = "GET"
return result
}()
As SE-0317 - async let bindings says:
...
async let
is similar to alet
, in that it defines a local constant that is initialized by the expression on the right-hand side of the=
. However, it differs in that the initializer expression is evaluated in a separate, concurrently-executing child task.The child task begins running as soon as the
async let
is encountered....
A
async let
creates a child-task, which inherits its parent task's priority as well as task-local values. Semantically, this is equivalent to creating a one-offTaskGroup
which spawns a single task and returns its result ...Similarly to the [
group.addTask
] function, the closure is@Sendable
andnonisolated
, meaning that it cannot access non-sendable state of the enclosing context. For example, it will result in a compile-time error, preventing a potential race condition, for aasync let
initializer to attempt mutating a closed-over variable:var localText: [String] = ... async let w = localText.removeLast() // error: mutation of captured var 'localText' in concurrently-executing code
The
async let
initializer may refer to any sendable state, same as any non-isolated sendable closure.
So, it is not the case that the parameter to data(for:delegate:)
is copied and then the asynchronous task is created, but rather the other way around.
Usually, if you were using a closure, you would just add request
to the closure’s capture list, but that’s not possible in this case. E.g., you could create a Task
yourself with a capture list, achieving something akin to async let
, but with greater control:
func test() async throws {
var request = URLRequest(url: URL(string:"https://httpbin.org/get")!)
request.httpMethod = "GET" // just for example
let task = Task { [request] in
try await URLSession.shared.data(for: request).0
}
// do some more stuff in parallel
print("Response body: \(String(data: try await task.value, encoding: .utf8) ?? "Not string")")
}
Obviously, you can simply await
the data(for:delegate:)
, rather than async let
, and the problem goes away:
func test() async throws {
var request = URLRequest(url: URL(string:"https://httpbin.org/get")!)
request.httpMethod = "GET" // just for example
let data = try await URLSession.shared.data(for: request).0
print("Response body: \(String(data: data, encoding: .utf8) ?? "Not string")")
}