7

I am trying to set up a publisher that will publish a set of integers and at some point may fail. It's slightly contrived but hopefully illustrates principle. Example below.

enum NumberError: Int, Error {
   case isFatal, canContinue
}

struct Numbers {
    let p = PassthroughSubject<Int, NumberError>()

    func start(max: Int) {

        let errorI = Int.random(in: 1...max)
        for i in (1...max) {
            if errorI == i {
                p.send(completion: .failure(NumberError.canContinue))
            } else {
                p.send(i)
            }
        }
        p.send(completion: .finished)

    }
}

I then subscribe using:

let n = Numbers()
let c = n.p
    .catch {_ in return Just(-1)}

    .sink(receiveCompletion: {result in
        switch result {
        case .failure:
            print("Error")
        case .finished:
            print("Finished")
        }
    }, receiveValue: {
        print($0)
    })

n.start(max: 5)

This works in that it replaces errors with -1 but I would then like to continue receiving values. Does anyone know if this is possible? Having read and looked around it seems that flatMap may be the way to go but I can't work out what publisher to use in the closure? Any help much appreciated.

E_mac
  • 113
  • 7

1 Answers1

8

I think you have an incorrect belief that a PassthroughSubject can publish more outputs after publishing a failure. It cannot. After you call p.send(completion: ...), any calls to p.send(...) will be ignored. Furthermore, if you subscribe to p after you call p.send(completion: ...), p will immediately complete the new subscription and not send any outputs.

So you can't send your error as .failure if you want to send more values after. Instead, change your publisher's Output type to Result<Int, NumberError> and its Failure type to Never:

import Combine

enum NumberError: Int, Error {
   case isFatal, canContinue
}

struct Numbers {
    let p = PassthroughSubject<Result<Int, NumberError>, Never>()

    func start(max: Int) {
        let bad = (max + 1) / 2
        for i in (1...max) {
            if bad == i {
                p.send(.failure(NumberError.canContinue))
            } else {
                p.send(.success(i))
            }
        }
        p.send(completion: .finished)
    }
}

But now you can't use catch to handle the error, because it's not coming through as a failure. Instead, you can use map:

let n = Numbers()
let c = n.p
    .map({
        switch $0 {
        case .success(let i): return i
        case .failure(_): return -1
        }
    })
    .sink(receiveCompletion: {result in
        switch result {
        case .failure:
            print("Error")
        case .finished:
            print("Finished")
        }
    }, receiveValue: {
        print($0)
    })

n.start(max: 5)

Output:

1
2
-1
4
5
Finished
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Thanks very much for the explanation and the alternative approach. Do you by any chance know how the example in WWDC 2019 Combine in practice recovers then? I believe they use flatMap to recover from an encode/decode error. – E_mac May 30 '20 at 21:07
  • In that session, the root publisher is a `NotificationCenter.Publisher`, which never fails. The failure in that WWDC example occurs inside the `flatMap` and is recovered using `.catch` inside the `flatMap`. The failure doesn't escape the `flatMap`. In your question, your root publisher is a `PassthroughSubject` and you send a failure through it. – rob mayoff May 30 '20 at 21:36