Search code examples
swiftasync-awaitconcurrencytaskthrows

Execute multiple concurrent (async let) tasks and throw their error only when all of them are failed


For example I need to get 2 arrays of the same type from different sources. Both of these requests may fail but I need to show any error (given by server) when these requests are failed simultaneously.

Here is a common example but it throws an error when any of requests is failed:

func test() async throws -> [Any] {
    async let t1 = getArr()
    async let t2 = getArr2()
    return try await t1 + t2
}

func getArr() async throws -> [Any] {
    []
}

func getArr2() async throws -> [Any] {
    []
}

In the same time if I use try? I lose the error returned by request.

I could also remove throws and replace returning result with Result<[Any], Error> but it looks strange.

How to resolve this issue correctly?


Solution

  • You definitely should use Result here. For convenience, first write a Result initialiser that catches errors in an async throwing closure.

    extension Result {
        init(asyncCatching block: () async throws -> Success) async where Failure == Error {
            do {
                self = .success(try await block())
            } catch {
                self = .failure(error)
            }
        }
    }
    

    Assuming you want test to throw all the errors when all of the async lets threw, make [Error] an Error type too.

    extension Array: Error where Element: Error {}
    

    Then it is as simple as:

    func test() async throws -> [Any] {
        async let t1 = Result { try await getArr() }
        async let t2 = Result { try await getArr2() }
        switch (await t1, await t2) {
        case let (.failure(e1), .failure(e2)):
            return [e1, e2]
        case let (.success(s), .failure), let (.failure, .success(s)):
            return s
        case let (.success(s1), .success(s2)):
            return s1 + s2
        }
    }
    

    For a more general version of this "reducing a collection of Results into a single value or throw" operation, you can write:

    func accumulateResults<T, U, Failure: Error>(_ results: [Result<T, Failure>], identity: U, reduce: (U, T) -> U) throws -> U {
        var ret = identity
        var success = false
        var failures = [Failure]()
        for result in results {
            switch result {
            case .success(let s):
                ret = reduce(ret, s)
            case .failure(let e):
                if success { break }
                failures.append(e)
            }
        }
        return if success { ret } else { throw failures }
    }
    

    Without using Result (or reinventing something similar to it), test can only throw the last error, because you cannot store any previous errors (the exact job of Result).

    func testWithoutResult() async throws -> [Any] {
        async let t1 = getArr()
        async let t2 = getArr2()
        var result = [Any]()
        var success = false
        do {
            result.append(try await t1)
            success = true
        } catch {}
        do {
            result.append(try await t2)
        } catch {
            if !success { throw error }
        }
        return result
    }
    

    For every non-final async operation, write do block and set success = true. For the final async operation, if !success { throw error } in the catch block instead.