SolvedRxSwift What is the correct way to handle errors?

Hi, I'm almost new to Rx and trying to understand the philosophy of reactive programming. 😄
I encountered the problem in error handling. I read many articles such as #316, #618 but I could not figure out how to handle errors without nesting flatMap or using Result model.

The code below is very similar to GitHubSignUp example in RxExample project. User inputs are passed to usernameInputDidReturn, passwordInputDidReturn, loginButtonDidTap, and login result will be sended back using didComplete observable.

LoginViewController subscribes didComplete directly, not nesting under self.usernameInput.rx_controlEvent or self.loginButton.rx_tap. How can we handle errors in this case?

Currently I'm using the Result model (as @frogcjn mentioned in #316), but I'd like to know if there is more reactive way.

LoginViewController.swift

// Input
self.usernameInput.rx_controlEvent(.EditingDidEndOnExit)
    .bindTo(self.viewModel.usernameInputDidReturn)
    .addDisposableTo(self.disposeBag)

self.passwordInput.rx_controlEvent(.EditingDidEndOnExit)
    .bindTo(self.viewModel.passwordInputDidReturn)
    .addDisposableTo(self.disposeBag)

self.loginButton.rx_tap
    .bindTo(self.viewModel.loginButtonDidTap)
    .addDisposableTo(self.disposeBag)

// Output
self.viewModel.didComplete
    .catchError { [weak self] error in
        // How can I handle error here? I'd like to handle error instance to provide user feedback.
        // It doesn't work but I'd like to do something like this:
        let message = (error as? LoginError)?.message
        self?.displayErrorLabel(message)
    }
    .subscribeNext { [weak self] in
        self?.startNextViewController()
    }
    .addDisposableTo(self.disposeBag)

LoginViewModel.swift

let usernameAndPassword = Observable
    .combineLatest(self.username.asObservable(), self.password.asObservable()) { username, password in
        return (username, password)
    }

// Observable<User>
self.didComplete = Observable.of(self.usernameInputDidReturn,
                                 self.passwordInputDidReturn,
                                 self.loginButtonDidTap)
    .merge()
    .withLatestFrom(usernameAndPassword)
    .flatMapLatest { username, password in
        // func api.login(...) -> Observable<User>
        return api.login(username: username, password: password) // this can emit error
    }

As @kzaher's comment

button.rx_tap
    .flatMapLatest { _ in
         return doManyThings()
               .catchError { handleErrors($0) }
    }
    .subscribeNext { input in
        // do something
    }

I should use such like this in LoginViewModel.swift:

self.didComplete = Observable.of(self.usernameInputDidReturn,
                                 self.passwordInputDidReturn,
                                 self.loginButtonDidTap)
    .merge()
    .withLatestFrom(usernameAndPassword)
    .flatMapLatest { username, password in
        return api.login(username: username, password: password)
            .catchError { handleErrors($0) } // <- catch errors here
    }

Then how can LoginViewModel tell LoginViewController that login has failed?

16 Answers

✔️Accepted Answer

I got an answer from the conversation with @kzaher on Slack. This key idea is: "Treat an API error as a 'failure of sequence' or just an 'error-representing' element."

How I have done is to return Observable<Result<User>> instead of Observable<User> from API function and treat API error as Result.Failure.

API.swift

enum Result<Value> {
    case Success(Value)
    case Failure(ErrorType)
}

func login(username username: String, password: String) -> Observable<Result<User>> {
    return ...
}

LoginViewController.swift

self.didComplete = Observable.of(self.usernameInputDidReturn,
                                 self.passwordInputDidReturn,
                                 self.loginButtonDidTap)
    .merge()
    .withLatestFrom(usernameAndPassword)
    .flatMapLatest { username, password in
        return api.login(username: username, password: password) // Observable<Result<User>>
    }
    .asDriver { error in
        return Driver.just(.Failure(error))
    }

LoginViewModel.swift

self.viewModel.didComplete
    .driveNext { result in
        switch result {
        case .Success(let user):
            self.processNextStep(user)

        case .Failure(let error):
            switch error {
            case LoginError.Username(let message):
                self.showError(message, on: self.usernameInput)

            case LoginError.Password(let message):
                self.showError(message, on: self.passwordInput)

            default:
                self.showError("Unknown error")
            }
        }
    }
    .addDisposableTo(self.disposeBag)

I attach the whole conversation for others 😄

@kzaher
I think, it’s about definition. This might sound weird at first but there is no such thing as universal error. You can probably just define error in a particular context. So if we are saying error in context of observable sequence, then yes, you obviously don’t want to terminate sequence.

What you want is an enum value that expresses that condition as sequence element. Result can emulate that ofc
but it’s not expandable. What you probably want is:

enum {
    case NormalCase
    case PresentErrorBecauseOf
    case PresentSomething3
}

And yes, if you have only kind of 2 cases, then you could abuse Result for this. So what we're doing is actually ​_FSP_​ (Functional Sequential Programming) :)

@devxoul
Does it mean that it is not always needed to use sequence's Error, but we can use element as an error case? Such as Result model or enum model as you mentioned.

@kzaher
I think this is more of o philosophical question :) I wouldn’t call this an error case. You just need to present a special case

@devxoul
Oh I got it. It is more similar to: Should REST API return 4xx status code on error? Someone says 'HTTP request has succeeded', other says 'HTTP request has succeeded but execution has failed'
Ummmm I cannot explain what I'm thinking in English 😞 But I think you're correct.

@kzaher
It’s is similar in that it also asks question, error in what layer. Error in what context, then yes :)
In this case, it’s error in your “applicative layer” and observable sequence would be HTTP layer :)
So the real problem is referring to both of these concepts as just error instead of error in observable sequence context (HTTP context) Error in my application context. Hope this clears things up :)

@devxoul
Cool. It made my brain open. Thanks! I can attach this conversation in #729 for others

@kzaher
thnx

Other Answers:

There was a comment here by @kean which was deleted. Went as follows:

I'm really struggling with error handling. RxSwift recommends to use Single for representing network requests and model errors as sequence errors.

func getRepo(_ repo: String) -> Single<[String: Any]>

But based on this thread, it turns out, this is incompatible with the idea of how to do error handling, which is to never allow sequences to fail. What's the point of using Single then? Wouldn't it make more sense to model Single such that it never fails in the first place (and maybe model all of the rest of the traits/subjects this way)? Has anyone explored this idea?

It was important for me to answer your question because I think you got the wrong idea. There is no idea of "you should never allow sequences to fail" that you should follow.

You need to handle and hone your errors, though. What does that mean?

  • If your stream feeds a UI Element, you wouldn't want it to ever emit an error, because UI Elements have no idea what to do with errors, and also UI elements should always have something on them. This is why things like Driver and Signal exist. You could easily achieve the same with a regular Observable obviously, which is the base for everything. You don't have to use traits or any other fancy types, they are just things that provide type-safe guarantees which make them easier for consumers to make assumptions about, but they're not a must.

  • If you have something that may have an error, and used by a different piece of you code, like a network request, I personally would have it throw that error. The consumer that uses your network request should decide what to do with the errors. Catch them? Materialize? etc. So that means, the inner units of your app may and should usually throw their errors but once you pass the data on to consumers, you should decide what to do in that error case.

  • When you need to handle errors as a user-facing event, the two most common ways are using Observable<Result<[String: Any], SomeError>>, or using materialize() on a regular Observable<[String: Any]> and splitting errors and elements. Me and my team personally prefer the latter as it provides more control (and basically an Event has the same shape of a Result, minus the typed error).

Hope this helps clear up some of the ideas (my personal thoughts, at least) about how to leverage error handling.

@devxoul I finally found dematerialize

More Issues: