8

I am trying to decode the downloaded JSON into a structure with the following code.

static func request(url: URL) -> AnyPublisher<SomeDecodableStruct, Error> {
    return URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .decode(type: SomeDecodableStruct.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

However, if processing fails, I would like you to return information on whether the request processing failed or the decoding processing failed. Therefore, I defined the FailureReason enum that conforms to the Error protocol as follows.

enum FailureReason : Error {
    case sessionFailed(error: URLError)
    case decodingFailed
}

static func request(url: URL) -> AnyPublisher<SomeDecodableStruct, FailureReason> {
    // ???
}

How do I define a request(url:) that satisfies this FailureReason?

omattyao
  • 95
  • 2
  • 6

2 Answers2

12

Combine is strongly typed with respect to errors, so you must transform your errors to the correct type using mapError or be sloppy like RxSwift and decay everything to Error.

enum NetworkService {
  enum FailureReason : Error {
      case sessionFailed(error: URLError)
      case decodingFailed
      case other(Error)
  }

  static func request<SomeDecodable: Decodable>(url: URL) -> AnyPublisher<SomeDecodable, FailureReason> {
    return URLSession.shared.dataTaskPublisher(for: url)
      .map(\.data)
      .decode(type: SomeDecodable.self, decoder: JSONDecoder())
      .mapError({ error in
        switch error {
        case is Swift.DecodingError:
          return .decodingFailed
        case let urlError as URLError:
          return .sessionFailed(error: urlError)
        default:
          return .other(error)
        }
      })
      .eraseToAnyPublisher()
  }
}
Josh Homann
  • 15,933
  • 3
  • 30
  • 33
  • 3
    great solution! Just one question, I see the first thing is to map data ```.map(\.data)```, so the response from output of ```dataTaskPublisher``` is ignored, then if some API return an error like HTTP 404, how could we catch it? – Zhou Haibo Apr 30 '20 at 06:21
  • @ChuckZHB using catch operator and Fail publisher: `.catch { Fail(...) }` – JAHelia Nov 02 '20 at 10:26
4

In this situation I wouldn't declare the publisher with other Failure type than Never. Otherwise the Publisher will send a completion with first error it encounters and stop publishing altogether. It is much better to make the Output of type Result. After each step which can produce an error you map it to your Error type using .mapError and as the last thing catch the error and return Result.failure

func request(url: URL) -> AnyPublisher<Result<SomeDecodableStruct, FailureReason>, Never> {
        return URLSession.shared.dataTaskPublisher(for: url)
                    .mapError { Error.sessionFailed(error: $0) }
                    .map { $0.data }
                    .decode(type: SomeDecodableStruct.self, decoder: JSONDecoder())
                    .map { Result<SomeDecodableStruct, FailureReason>.success($0)}
                    .mapError { _ in Error.decodingFailed }
                    .catch { Just<Result<SomeDecodableStruct, FailureReason>>(.failure($0)) }
                    .eraseToAnyPublisher()
    }
LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
  • Thank you for answering. The idea of using `Result` is great. I have a question. Does the source code convert `.sessionFailure` to `decodingFailure` by the second `mapError`? – omattyao Oct 06 '19 at 16:14
  • .mapError changes an error that can be thrown at this time (in this example) to any other type error type. Since your FailureReason has sessionFailure declare as having an associated type of URLError you need it to pass it in when you initialise this case. – LuLuGaGa Oct 06 '19 at 17:28
  • I would not materialize the error to a result unless if you are recycling this publisher (ie its inside of flatMap). Even then there is usually a better pattern for catching errors than using materialize or manually materializing with a Result. – Josh Homann Oct 08 '19 at 01:02
  • @JoshHomann I don't face issues with manually materializing the error this way. – JAHelia Feb 17 '20 at 19:33