What is the best way to handle errors in Combine?

Viewed 6933

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?

2 Answers

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()
  }
}

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()
    }
Related