Skip to content

Instantly share code, notes, and snippets.

@skoirala
Last active April 4, 2018 18:15
Show Gist options
  • Save skoirala/4f543f864b8381593b1034abb660eeb3 to your computer and use it in GitHub Desktop.
Save skoirala/4f543f864b8381593b1034abb660eeb3 to your computer and use it in GitHub Desktop.
Dealing with Error in RxSwift
@skoirala
Copy link
Author

skoirala commented Apr 4, 2018

Handling Error in RxSwift

Support we have the following code,

let result = Observable.merge([successButton.rx.tap.asObservable().map { _ in true },
                                   failureButton.rx.tap.asObservable().map { _ in false }])
                       .flatMap { [unowned self] shouldNotThrowError in
                            self.performApiCall(shouldNotThrowError: shouldNotThrowError)
                         }

    
let successLabelText = result.scan(0) { a, _ in a + 1}.map { "\($0)" }
    
successLabelText.bind(to: successLabel.rx.text)
                .disposed(by: disposeBag)

The method performApiCall can throw error and can fail.

func performApiCall(shouldNotThrowError: Bool) -> Observable<()> {
    if shouldNotThrowError {
        return .just(())
    } else {
        return .error(SomeError.buttonError)
    }
}

If success button is pressed for few times, it can be seen that successLabel correctly shows the changes with number of times clicked with successbutton. As soon as failure button is clicked, the performApiCall throws error and so, the sequence completes at this time. Now, further pressing successButton or failureButton wont do anything, since the sequence already completed.

How can this be fixed ? Such that we can catch the error and not let the sequence to complete ?

There are various ways to deal with this, listed as follows:

Using catchErrorJustReturn(element:)

This method allows to catch the error from failing sequences and return any element.

Lets say we make change to the code a bit such that we can box the result. Lets define an enum State

enum Status {
    case success
    case failed
}

The gist here is to catch the error and return failed status such that sequence does not complete.

The modification to the code,

let result = Observable.merge([successButton.rx.tap.asObservable().map { _ in Status.success },
                               failureButton.rx.tap.asObservable().map { _ in Status.failed }])
            .flatMap { [unowned self] status in
                    self.performApiCall(status: status)
                        .map {_ in Status.success }
                        .catchErrorJustReturn(Status.failed)
            }


let successLabelText = result.scan(0) { a, status in
    if case .failed = status {
        return a
    }
    return a + 1
}.map { "\($0)"}

let failedLabelText = result.scan(0) { a, status in
    if case .success = status {
        return a
    }
    return a + 1
    }.map { "\($0)"}

successLabelText.bind(to: successLabel.rx.text)
                .disposed(by: disposeBag)
failedLabelText.bind(to: failureLabel.rx.text)
    .disposed(by: disposeBag)

performApiCall(status:) still remains unchanged except an small change such that it can accept status.

func performApiCall(status: Status) -> Observable<()> {
    switch status {
    case .success:
        return .just(())
    case .failed:
        return .error(SomeError.buttonError)
    }
}

Now, this will correctly reflect inside label.

Using catchError(handler: (Error) throws -> Observable<E>)

This works very similar to the above catchErrorJustReturn(element:), except it passes a closure which can return an Observable of element. Only small modification to the above code would be needed to use this method.

let result = Observable.merge([successButton.rx.tap.asObservable().map { _ in Status.success },
                               failureButton.rx.tap.asObservable().map { _ in Status.failed }])
            .flatMap { [unowned self] status in
                    self.performApiCall(status: status)
                        .map {_ in Status.success }
                        .catchError{ error in
                                return .just(.failed)
                        }
            }

Using materialize() and dematerialize() :

materialize() wraps events in an Event. The returned Observable never errors, but it does complete after observing all of the events of the underlying Observable.

dematerialize() convert any previously materialized Observable into it’s original form.

The original throwing Observable can be materialized and later dematerialized such that error is not thrown and sequence wont complete immediately.

materliazed sequence have property element which is of type optional. The value of this property is nil if error is thrown, if property is not nil, we can be sure that no error was thrown. This can be used to not stop sequence from completing.

let result = Observable.merge([successButton.rx.tap.asObservable().map { _ in true },
                               failureButton.rx.tap.asObservable().map { _ in false }])
            .flatMap { [unowned self] shouldNotThrowError in
                    self.performApiCall(shouldNotThrow: shouldNotThrowError)
                        .materialize()
            }.share()

    let successLabelText = result.filter { $0.element != nil }
        .scan(0) { a, _ in a + 1}
        .map { "\($0)" }
    
    let failedLabelText = result.filter { $0.element == nil && !$0.isCompleted }
        .scan(0) { a, _ in a + 1}
        .map { "\($0)" }

Notice, how materialize helps in dealing with error. The filter used in filtering error also checks if isCompleted, since materializing sequence sends next and completes it immediately, which also has element property nil. So, completed events are not taken into account when dealing with error

The example shows simple usecase. The status and error handling can be extended to bind the error to some variable or then box the error and completed sequences such that Observer can deal with the error sensibly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment