How to return a failure inside .map function in a Result

Viewed 247

I have a method execute that calls an external API with a callback that receives Result<Data?,Error>. How can I map that optional success to an unwrapped result or an Error?

func execute(then handle: @escaping (Result<Data, Error>) -> Void) {
    externalAPI.retrieveData { result in
        let mappedResult = result
            .map {
                guard let data = $0 else {
                    throw NSError(domain: "", code: 0, description: "error")
                }
                return data
            }
        handle(mappedResult)
    }
}

This code fails with Invalid conversion from throwing function of type '(Optional<Data>) throws -> _' to non-throwing function type '(Data?) -> NewSuccess'

I was able to do this with a simple switch (below), but I was wondering if throwing a failure inside the .map is possible.

func execute(then handle: @escaping (Result<Data, Error>) -> Void) {
    externalAPI.retrieveData { result in
        switch result {
        case .failure(let error):
            handle(.failure(error))
        case .success(let data):
            guard let data = data else {
                handle(.failure(NSError(domain: "", code: 0, description: "error")))
                return
            }
            handle(.success(data))
        }
    }
}
2 Answers

You can convert between throws functions and functions that return Result<Success, Error> by using Result(catching:) and .get().

Here's your original map call:

.map {
    guard let data = $0 else {
        throw NSError(domain: "", code: 0, description: "error")
    }
    return data
}

Result.map takes a Result and a function that converts (Success) -> NewSuccess, and returns a Result<NewSuccess, Failure>.

Your map takes a Data (Success), and returns Result<Data, Error> (NewSuccess). So the final type, by plugging in NewSuccess is: Result<Result<Data, Error>, Error>. That's more layers than you want. You want to flatten that to just Result<Data, Error>, and that's where flatMap comes in.

Your answer shows that, but you can also pull this out into a more general-purpose tool. It only works when Failure == Error, because throws is untyped, so you can't limit it to some subset of errors. But that's what you're doing anyway. Here's tryMap:

extension Result where Failure == Error {
    func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> {
        self.flatMap { value in
            Result<NewSuccess, Error> { try transform(value) }
        }
    }
}

With that, you can rewrite this as:

func execute(then handle: @escaping (Result<Data, Error>) -> Void) {
    externalAPI.retrieveData { result in
        handle(result
                .tryMap {
                    guard let data = $0 else {
                        throw NSError(domain: "", code: 0, description: "error")
                    }
                    return data
                })
    }
}

That said, I'd probably be tempted to write it this way:

func execute(then handle: @escaping (Result<Data, Error>) -> Void) {
    externalAPI.retrieveData { result in
        handle(result
                .flatMap { maybeData in
                    maybeData.map(Result.success)
                        ?? .failure(NSError(domain: "", code: 0, description: "error"))
                })
    }
}

Or if I wanted someone to be able to actually read it later:

func execute(then handle: @escaping (Result<Data, Error>) -> Void) {
    externalAPI.retrieveData { result in
        handle(result
                .flatMap {
                    switch $0 {
                    case .some(let data): return .success(data)
                    case .none: return .failure(NSError(domain: "", code: 0, description: "error"))
                    }
                }
        )
    }
}

The advantage of this switch over yours is that you don't have to unwrap and rewrap previous failures.

Apparently, this can be done using flatmap. So in my case:

func execute(then handle: @escaping (Result<Data, Error>) -> Void) {
    externalAPI.retrieveData { result in
        let mappedResult = result
            .flatMap { data in
                Result<Data, Error> {
                    guard let data = data else {
                        throw NSError(domain: "", code: 0, description: "error")
                    }
                    return data
                }
            }
         handle(mappedResult)
    }
}

It's a little confusing, but it is working for me.

Related