Rxswift: Handle error with flatMap

Created on 30 Mar 2017  路  10Comments  路  Source: ReactiveX/RxSwift

Hi, I have these errors and a flow with flatMap and drive

enum MyError: Error {
  case firstFlatMapLatest
  case innerFirstFlatMapLatest
  case secondFlatMapLatest
  case thirdFlatMap
}
button.rx.tap
      .flatMapLatest({
        return Observable.error(MyError.firstFlatMapLatest)
          .flatMapLatest({
            return Observable.error(MyError.innerFirstFlatMapLatest)
          })
          .catchErrorJustReturn(())
      })
      .flatMapLatest({
        return Observable.error(MyError.secondFlatMapLatest).catchErrorJustReturn(())
      })
      .flatMap({
        return Observable.error(MyError.thirdFlatMap).catchErrorJustReturn(())
      })
      .catchError({ error in
        print(error)
        return Observable.just(())
      })
      .map({
        return "hello"
      })
      .asDriver(onErrorRecover: { error in
        print(error)
        return Driver.just("hi")
      })
      .drive(onNext: { value in
        print(value)
      })

Here I must use catchErrorJustReturn for the flow to not be terminated, but what if I don't do catchErrorJustReturn?

  • Why can error inside flatMapLatest stop the whole flow? In this case catchError is only called once
  • Why do we need to catch error inside flatMapLatest?
  • I have an inner flatMapLatest inside the first flatMapLatest, why don't I need to catchErrorJustReturn for that? like
.flatMapLatest({
    return Observable.error(MyError.innerFirstFlatMapLatest).catchErrorJustReturn(())
})
  • Is there a difference when using flatMap and flatMapLatest here

This may related to https://github.com/ReactiveX/RxSwift/issues/618, https://github.com/ReactiveX/RxSwift/issues/729, https://github.com/ReactiveX/RxSwift/issues/304

Most helpful comment

The answer from @sergdort is right, but it's a little bit hard to understand. Therefore I make sample codes myself to test it. Basically:

  • Case_A: when you catchError outside flatMap, you are transforming outer observable(s) to new observable (that raises error) -> _the previous observable(s) will be completed due this error_ 鉂楋笍

  • Case_B: when you catchError inside flatMap, you are transforming outer observable(s) to another new observable (that raise error but covered ) -> _the previous observable(s) don't know about the error and continue to run!_ 馃槇

Note in examples below, I don't call .addDisposableTo(disposeBag). You can see [TAP] disposed itself (while it should dispose manually only when we call .dispose()).

enum MyError: Error {
    case fakeError
}

Case_A

 _ = buttonLogin.rx.tap.asObservable().debug("[TAP]")
            .flatMap {
                return Observable<Int>.error(MyError.fakeError)
            }.debug("[FLATMAP]")
            .catchErrorJustReturn(0).debug("[JUST]")
            .subscribe()    

// [TAP]        --T-|>
// [FLATMAP]    --X->
// [JUST]       --0-|>

Case_B

_ = buttonLogin.rx.tap.asObservable().debug("[TAP]")
            .flatMap {
                return Observable<Int>.error(MyError.fakeError).debug("[ERROR]")
                                .catchErrorJustReturn(0).debug("[JUST]")
            }.debug("[FLATMAP]")
            .subscribe()

// [TAP]       --T--------T----------T------------T--.....-->
// [ERROR]     --X->    --X->      --X->        --X->
// [JUST]      --0-|>   --0-|>     --0-|>       --0-|>
// [FLATMAP]   --0--------0----------0------------0--.....-->

Another case is covering error by catchError( return Observable). Then new observable will continue to run, while previous observable(s) will be completed.
```
_ = buttonLogin.rx.tap.asObservable().debug("[TAP]")
.flatMap {
return Observable.error(MyError.fakeError)
}.debug("[FLATMAP]")
.catchError { (error) in
return Observable.interval(1, scheduler: MainScheduler.instance)
}.debug("[INTERVAL]")
.subscribe()

// [TAP] --T-|>
// [FLATMAP] --X->
// [INTERVAL] --1---2---3---4---5---6---7---8---9--.....-->
````

All 10 comments

Hi @onmyway133 ,

The behavior you are describing is a consequence of sequence grammar.
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#getting-started

Safer approach would be to use Driver in this chain.

button.rx.tap.asDriver()
      .flatMapLatest

because compiler will create compile time warnings then.

@kzaher Hi, thanks for your reply. I think we only have a method called asDriver(onErrorJustReturn)?

And why don't we need to catch error in the inner flatMapLatest of the first flatMapLatest?

Hi, thanks for your reply. I think we only have a method called asDriver(onErrorJustReturn)?

Because ControlProperty/ControlEvent/Variable can't produce error, they also have asDriver() method.

And why don't we need to catch error in the inner flatMapLatest of the first flatMapLatest?

The behavior you are describing is a consequence of sequence grammar.
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#getting-started

return Observable.error(MyError.firstFlatMapLatest)
          .flatMapLatest({
            return Observable.error(MyError.innerFirstFlatMapLatest)
          })
          .catchErrorJustReturn(()) <--- returns a sequence that doesn't fail

@kzaher Hi, sorry if I ask too much 馃槆

In my example, I have .asDriver(onErrorRecover:, do I need to do it? If I need to do it, it seems that .asDriver(onErrorRecover and catchErrorJustReturn in flatMapLatest are trying to catch errors from 2 different sequences?

Hi @onmyway133 ,

I'm really sorry, this is an issues channel only. I've tried to help you and point you in the right direction, but please read the docs. Everything is documented regarding this, and there is a lot of examples in this repo.

@kzaher I read it many times but still don't get it 馃槩 , they are just very simple examples. @icanzilb maybe you can help me understand in layman 's term? I've read many other tutorials, and they recommend the same (catchErrorJustReturn inside flatMap) without explanation

I think catchError is enough to catch the error, but with flatMap we need to catch the error inside, which I don't really understand. I can just blindly follow and it works, but an explanation would be great 馃槆

Hi, @onmyway133 this and this is why it does not happened. When you catch inner sequence it completes and decrements number of active sequences but since outer sequence is probably UI related e.g button taps or text field values they are only complete on deinit that's why sequence is not stopped

But if you catch outer sequence e.g. by doing

button.rx.tap().flatMap {
    return //some observable
}.catchErrorJustReturn(something)

it will catch error of outer sequence and complete it . and if it e.g button this will remove RxTarget from the button thats why it won't emit anything

From my experience I can highly recommend reading implementation in case you confused with operator behaviour. It really helped me a lot to understand how everything works

@sergdort Hi, many thanks for answering. I'm checking it out. Will write a detailed explanation about this once I understand it

The answer from @sergdort is right, but it's a little bit hard to understand. Therefore I make sample codes myself to test it. Basically:

  • Case_A: when you catchError outside flatMap, you are transforming outer observable(s) to new observable (that raises error) -> _the previous observable(s) will be completed due this error_ 鉂楋笍

  • Case_B: when you catchError inside flatMap, you are transforming outer observable(s) to another new observable (that raise error but covered ) -> _the previous observable(s) don't know about the error and continue to run!_ 馃槇

Note in examples below, I don't call .addDisposableTo(disposeBag). You can see [TAP] disposed itself (while it should dispose manually only when we call .dispose()).

enum MyError: Error {
    case fakeError
}

Case_A

 _ = buttonLogin.rx.tap.asObservable().debug("[TAP]")
            .flatMap {
                return Observable<Int>.error(MyError.fakeError)
            }.debug("[FLATMAP]")
            .catchErrorJustReturn(0).debug("[JUST]")
            .subscribe()    

// [TAP]        --T-|>
// [FLATMAP]    --X->
// [JUST]       --0-|>

Case_B

_ = buttonLogin.rx.tap.asObservable().debug("[TAP]")
            .flatMap {
                return Observable<Int>.error(MyError.fakeError).debug("[ERROR]")
                                .catchErrorJustReturn(0).debug("[JUST]")
            }.debug("[FLATMAP]")
            .subscribe()

// [TAP]       --T--------T----------T------------T--.....-->
// [ERROR]     --X->    --X->      --X->        --X->
// [JUST]      --0-|>   --0-|>     --0-|>       --0-|>
// [FLATMAP]   --0--------0----------0------------0--.....-->

Another case is covering error by catchError( return Observable). Then new observable will continue to run, while previous observable(s) will be completed.
```
_ = buttonLogin.rx.tap.asObservable().debug("[TAP]")
.flatMap {
return Observable.error(MyError.fakeError)
}.debug("[FLATMAP]")
.catchError { (error) in
return Observable.interval(1, scheduler: MainScheduler.instance)
}.debug("[INTERVAL]")
.subscribe()

// [TAP] --T-|>
// [FLATMAP] --X->
// [INTERVAL] --1---2---3---4---5---6---7---8---9--.....-->
````

Was this page helpful?
0 / 5 - 0 ratings

Related issues

trungp picture trungp  路  3Comments

retsohuang picture retsohuang  路  3Comments

RafaelPlantard picture RafaelPlantard  路  3Comments

jaumard picture jaumard  路  3Comments

jeremiegirault picture jeremiegirault  路  3Comments