Problem
When processing collections with asynchronous transformations, the standard map method falls short because its closures cannot be async.
A naive solution would be to use a for loop with await, but this sacrifices readability and consistent error handling.
The goal is to have a sequential map that accepts async throws functions, preserves element order, and simplifies the workflow.
Solution
We create an extension on Sequence that adds asyncMap.
Its execution is sequential within the same task, which allows us to:
- Maintain deterministic ordering of results.
- Limit resource consumption by executing only one operation at a time.
- Uniformly propagate the first error that occurs, stopping the process.
extension Sequence {
@inlinable
func asyncMap<T>(
_ transform: @escaping @Sendable (Element) async throws -> T
) async throws -> [T] {
var results: [T] = []
results.reserveCapacity(underestimatedCount)
for element in self {
let value = try await transform(element)
results.append(value)
}
return results
}
}
Result
With asyncMap, we achieve a clear and safe workflow for asynchronous transformations without parallelism.
It’s especially useful when:
- We need guaranteed ordering.
- We must limit resource consumption (one task in flight).
- There’s dependency between operations.
If you need parallel execution instead of sequential, I explored that approach in Concurrent Map. And if your sequential calls hit rate limits, I added a timeout mechanism in Async Map Timeout.
Usage example:
let urls: [URL] = [...]
let contents = try await urls.asyncMap {
try await fetchContent(from: $0)
}
Keep coding, keep running 🏃♂️