Async Map


My 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.

My 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
    }
}

My 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 πŸƒβ€β™‚οΈ