Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to handle errors in Combine?

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?

like image 868
omattyao Avatar asked Oct 05 '19 19:10

omattyao


People also ask

What are the error handling methods?

Error-handling techniques for logic errors or bugs is usually by meticulous application debugging or troubleshooting. Error-handling applications can resolve runtime errors or have their impact minimized by adopting reasonable countermeasures depending on the environment.

How do I capture a fatal error in Swift?

There are four ways to handle errors in Swift. You can propagate the error from a function to the code that calls that function, handle the error using a do - catch statement, handle the error as an optional value, or assert that the error will not occur.

What is sink in combine?

What's a sink? While a complete explanation of Combine, publishers, subscribers, and sinks is beyond the scope of this article, for our purposes here it's probably enough to know that in Combine a sink is the code receives data and completion events or errors from a publisher and deals with them.


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()
  }
}
like image 172
Josh Homann Avatar answered Sep 21 '22 16:09

Josh Homann


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()
    }
like image 45
LuLuGaGa Avatar answered Sep 21 '22 16:09

LuLuGaGa