32

Given the following code:

    enum MyError: Error {
        case someError
    }

    myButton.publisher(for: .touchUpInside).tryMap({ _ in
        if Bool.random() {
            throw MyError.someError
        } else {
            return "we're in the else case"
        }
    })
        .replaceError(with: "replaced Error")
        .sink(receiveCompletion: { (completed) in
            print(completed)
        }, receiveValue: { (sadf) in
            print(sadf)
        }).store(in: &cancellables)

Whenever I tap the button, I get we're in the else case until Bool.random() is true - now an error is thrown. I tried different things, but I couldn't achieve to catch/replace/ignore the error and just continue after tapping the button.

In the code example I would love to have e.g. the following output

we're in the else case
we're in the else case
replaced Error
we're in the else case
...

instead I get finished after the replaced error and no events are emitted.

Edit Given a publisher with AnyPublisher<String, Error>, how can I transform it to a AnyPublisher<String, Never> without completing when an error occurs, i.e. ignore errors emitted by the original publisher?

swalkner
  • 16,679
  • 31
  • 123
  • 210
  • You need to use catch{} – E.Coms Nov 03 '19 at 06:16
  • but what to write in the Catch-block? If I use a Just, the publisher finishes as wel – swalkner Nov 03 '19 at 18:31
  • It’s a good question, what you expect is a new publisher which is as same as current one. In a common case , maybe ‘sink’ is not an ideal subscriber here. Try a subject before sink – E.Coms Nov 04 '19 at 01:00
  • you mean a custom subject, i.e. one that only "forwards" values and not errors? – swalkner Nov 04 '19 at 07:14
  • 1
    I found the answer now, just use FlatMap , check WWDC videos – E.Coms Nov 10 '19 at 21:32
  • do you have a link to the exact timetable or a short example? – swalkner Nov 11 '19 at 21:25
  • @swalkner how did you define myButton? – Vyacheslav Nov 12 '19 at 21:05
  • it's from the storyboard: `@IBOutlet var myButton: UIButton!` – swalkner Nov 12 '19 at 21:06
  • @swalkner I've posted my answer – Vyacheslav Nov 12 '19 at 21:14
  • I see you problem now. You need to divide your initial publisher to two parts. One is out of flatMap, out is insider flatMap. For example, if you can produce a publisher with one parameter , you do can something like this: Just(parameter).flatMap{ (value)->AnyPublisher in return MyPublisher(value).catch { () } }.sink(....) // MyPublisher(value) will give you a AnyPublisher. Catch can convert it to a AnyPublisher. FlatMap will keep producing new publishers and never finishes – E.Coms Nov 13 '19 at 14:56
  • @E.Coms I don't get it, can you update your answer please to have it formatted? – swalkner Nov 13 '19 at 15:46
  • I've added a new answer also including a link to the WWDC'19 movie mentioned above. Feel free to accept it as the correct answer if you feel it helps. – smat88dd Aug 06 '20 at 10:03
  • You can return your own error in catch block: catch(let error) { return Fail(error: error) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } – Illya Krit Oct 13 '20 at 08:13

8 Answers8

32

There was a WWDC movie mentioned, and I believe it's "Combine in Practice" from 2019, start watching around 6:24: https://developer.apple.com/wwdc19/721

Yes, .catch() terminates the upstream publisher (movie 7:45) and replaces it with a given one in the arguments to .catch thus usually resulting in .finished being delivered when using Just() as the replacement publisher.

If the original publisher should continue to work after a failure, a construct involving .flatMap() is requried (movie 9:34). The operator resulting in a possible failure needs to be executed within the .flatMap, and can be processed there if necessary. The trick is to use

.flatMap { data in
    return Just(data).decode(...).catch { Just(replacement) }
}

instead of

.catch { return Just(replacement) } // DOES STOP UPSTREAM PUBLISHER

Inside .flatMap you always replace the publisher and thus do not care if that replacement publisher is terminated by .catch, since its already a replacement and our original upstream publisher is safe. This example is from the movie.

This is also the answer to your Edit: question, on how to turn a <Output, Error> into <Output, Never>, since the .flatMap does not output any errors, its Never before and after the flatMap. All error-related steps are encapsulated in the flatMap. (Hint to check for Failure=Never: if you get Xcode autocompletion for .assign(to:) then I believe you have a Failure=Never stream, that subscriber is not available otherwise. And finally the full playground code

PlaygroundSupport.PlaygroundPage.current.needsIndefiniteExecution = true

enum MyError: Error {
    case someError
}
let cancellable = Timer.publish(every: 1, on: .main, in: .default)
    .autoconnect()
    .flatMap({ (input) in
        Just(input)
            .tryMap({ (input) -> String in
                if Bool.random() {
                    throw MyError.someError
                } else {
                    return "we're in the else case"
                }
            })
            .catch { (error) in
                Just("replaced error")
        }
    })
    .sink(receiveCompletion: { (completion) in
        print(completion)
        PlaygroundSupport.PlaygroundPage.current.finishExecution()
    }) { (output) in
        print(output)
}
smat88dd
  • 2,258
  • 2
  • 25
  • 38
11

I believe E. Coms answer is correct, but I'll state it much simpler. The key to handling errors without causing the pipeline to stop processing values after an error is to nest your error-handling publisher inside of flatMap:

import UIKit
import Combine

enum MyError: Error {
  case someError
}

let cancel = [1,2,3]
  .publisher
  .flatMap { value in
    Just(value)
      .tryMap { value throws -> Int in
        if value == 2 { throw MyError.someError }
        return value
    }
    .replaceError(with: 666)
  }
  .sink(receiveCompletion: { (completed) in
    print(completed)
  }, receiveValue: { (sadf) in
    print(sadf)
  })

Output:

1
666
3
finished

You can run this example in a playground.


Regarding the OP's edit:

Edit Given a publisher with AnyPublisher<String, Error>, how can I transform it to a AnyPublisher<String, Never> without completing when an error occurs, i.e. ignore errors emitted by the original publisher?

You can't.

Gil Birman
  • 35,242
  • 14
  • 75
  • 119
  • "You can't." this is not true. Replace `.replaceError(with: 666)` with `.catch { _ in Empty() }` and your error is now gone – Simon Apr 14 '23 at 14:47
  • @Simon what about the "without completing when an error occurs" part? – Gil Birman Apr 14 '23 at 16:15
  • It doesn't complete on error, try it yourself. Your code snippet still works fine in a playground. The output is `1` `3` `finished`. – Simon Apr 14 '23 at 20:49
  • Ah OK, so you're ignoring the "original publisher" part of why "You can't". My code works because we're using flatMap and creating a new publisher. – Gil Birman Apr 17 '23 at 19:59
4

To do this you can use the catch operator and Empty publisher:

let stringErrorPublisher = Just("Hello")
    .setFailureType(to: Error.self)
    .eraseToAnyPublisher() // AnyPublisher<String, Error>

let stringPublisher = stringErrorPublisher
    .catch { _ in Empty<String, Never>() }
    .eraseToAnyPublisher() // AnyPublisher<String, Never>
  • 1
    here I'm facing the same problem. As soon as the initial publisher throws an error, the completion is called and no more values are emitted. – swalkner Nov 13 '19 at 14:36
  • 1
    There is an overload on the initializer for `Empty(completeImmediately: false) }` which stops it completing, would that help? –  Nov 13 '19 at 15:31
  • unfortunately, it doesn't. The completion block isn't called, but there are no values emitted either. – swalkner Nov 13 '19 at 15:45
  • 2
    Reading the docs for `catch` it says that it replaces the upstream publisher which explains why no more values are sent. Are you able to return a new version of your original publisher in the `catch` rather than an `Empty`? e.g.: `.catch { _ in myButton.publisher(for: .touchUpInside)... }` –  Nov 13 '19 at 16:13
0

Just insert flatMap as following and you can achieve what your want

   self.myButton.publisher(for: \.touchUpInside).flatMap{
            (data: Bool) in
        return Just(data).tryMap({ _ -> String in
        if Bool.random() {
            throw MyError.someError
        } else {
            return "we're in the else case"
        }}).replaceError(with: "replaced Error")
    }.sink(receiveCompletion: { (completed) in
            print(completed)
        }, receiveValue: { (sadf) in
            print(sadf)
       }).store(in: &cancellables)

The working model seems like this:

 Just(parameter).
 flatMap{ (value)->AnyPublisher<String, Never> in 
 return MyPublisher(value).catch { <String, Never>() } 
 }.sink(....)

If we use the above example, it could be like this:

let firstPublisher    = {(value: Int) -> AnyPublisher<String, Error> in
           Just(value).tryMap({ _ -> String in
           if Bool.random() {
               throw MyError.someError
           } else {
               return "we're in the else case"
            }}).eraseToAnyPublisher()
    }

    Just(1).flatMap{ (value: Int) in
        return  firstPublisher(value).replaceError(with: "replaced Error")
   }.sink(receiveCompletion: { (completed) in
            print(completed)
        }, receiveValue: { (sadf) in
            print(sadf)
       }).store(in: &cancellables)

Here, you can replace the firstPublisher with AnyPublisher that takes one parameter.

Also Here, the firstPublisher only has one value, it only can produce one value. But if your publisher can produce multiple values, it will not finish before all values has been emit.

E.Coms
  • 11,065
  • 2
  • 23
  • 35
  • that's not what I want... I would like to "erase" the error BEFORE the sink and AFTER the tryMap. TryMap was only used to simulate a publisher throwing an error. – swalkner Nov 12 '19 at 06:57
  • I also use the tryMap to simulate the error. You just put all the try/catch or try/replace or catch logic inside the flatmap block, it will not terminate the publisher even after the replaceerror. I think it is what you need. You may replace tryMap with any other one. – E.Coms Nov 12 '19 at 12:34
  • but you have the error throwing publisher inside the `flatMap`, that's not what I'm looking for. I'm looking for something where `let something: AnyPublisher = ...` can be transformed to `AnyPublisher` - or am I still missing something? – swalkner Nov 12 '19 at 21:00
  • FlatMap is such an operator. It prevents the publisher from terminating. ReplaceError can work too, but once it corrects the error, it will terminate the publisher. So you need this operator wrapped inside the FlatMap. Thus after the error has been corrected, the original publisher will be resumed. You can run the code to see if it is same you mentioned. – E.Coms Nov 12 '19 at 21:05
  • and if I do NOT have access to the original publisher (say it's already a `AnyPublisher`), how can I wrap that inside flatMap? – swalkner Nov 12 '19 at 21:08
  • I see. You mean you don’t have a publisher from UIButton? – E.Coms Nov 12 '19 at 21:11
  • You can put your publisher inside the FlatMap. – E.Coms Nov 12 '19 at 21:15
  • and what‘s the publisher on which flatMap is called? Did you try it out? – swalkner Nov 12 '19 at 21:17
  • Any publisher is fine before FlatMap and put your error publisher insider the FlatMap – E.Coms Nov 12 '19 at 21:19
  • that’s absolutely not what I want. I want this one publisher to transform, nothing else. – swalkner Nov 12 '19 at 21:20
  • Currently if you use your code, once the error is replaced the publisher will be terminated , and it prints finished .Well ,I make a UIButton publisher myself with ‘touchupinsder’ property. Then hook that to the FlatMap. It just keeps running even it got the error, it never prints finished. So The FlatMap is the operator you need here. – E.Coms Nov 12 '19 at 21:29
0

I was struggling with that problem too, and I finally found a solution. One thing to understand is that you cannot recover from a completed flux. The solution is to return a Result instead of an Error.

let button = UIButton()
button.publisher(for: .touchUpInside)
    .map({ control -> Result<String, Error> in
        if Bool.random() {
            return .failure(MyError.someError)
        } else {
            return .success("we're in the else case")
        }
    }).sink (receiveValue: { (result) in
        switch(result) {
        case .success(let value):
            print("Received value: \(value)")
        case .failure(let error):
            print("Failure: \(String(describing: error))")
        }
    })

For anyone else reading this thread and trying to compile the code, you will need to import the code from this article (for the button.publisher(for: .touchUpIsinde) part).

Bonus, here's the code to handle errors with a PassthroughSubject and never complete the flux:

let subscriber = PassthroughSubject<Result<String, MyError>, Never>()

subscriber
    .sink(receiveValue: { result in
        switch result {
        case .success(let value):
            print("Received value: \(value)")
        case .failure(let error):
            print("Failure: \(String(describing: error))")
        }
    })

You cannot use PassthroughSubject<String, MyError>() directly, otherwise, the flux will complete when an error occurs.

0

The best way to not complete pipeline and to handle gracefully errors I found in this Peter Friese article: https://peterfriese.dev/posts/swiftui-combine-networking-errorhandling/ by using this Publisher extension method to map what we receive to Result:

extension Publisher {
  func asResult() -> AnyPublisher<Result<Output, Failure>, Never> {
    self
      .map(Result.success)
      .catch { error in
        Just(.failure(error))
      }
      .eraseToAnyPublisher()
  }
}

Then you can use it something like that:

publisher.flatMap {term in
            somePublisherWhichGivesError
                .asResult()
        }
        .sink(receiveValue: { value in
            switch value {
            case let .failure(error): self.handleError(error)
            case let .success(data): self.handleSuccess(data)
            }
        }).store(in: bag)
Michael Katkov
  • 2,256
  • 1
  • 20
  • 17
-1

I suggest using Publisher with typealias Failure = Never and output as an optional Result: typealias Output = Result<YourSuccessType, YourFailtureType>

Vyacheslav
  • 26,359
  • 19
  • 112
  • 194
-1

The Publisher emits until it completes or fails (with an error), after that the stream will be terminated.

One way to overcome this is to use a Result as a Publisher type

Publisher

protocol SerivceProtocol {
    var value: Published<Result<Double, MyError>>.Publisher { get }
}

Subscriber

service.value
        .sink { [weak self] in
            switch $0 {
            case let .success(value): self?.receiveServiceValue(value)
            case let .failure(error): self?.receiveServiceError(error)
            }
        }
        .store(in: &subscriptions)
Igor Kovryzhkin
  • 2,195
  • 1
  • 27
  • 22