What is the correct way to handle errors? #729

Closed
devxoul opened this Issue Jun 7, 2016 · 6 comments

Comments

Projects
None yet
3 participants
@devxoul
Collaborator

devxoul commented Jun 7, 2016

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?

@devxoul

This comment has been minimized.

Show comment
Hide comment
@devxoul

devxoul Jun 7, 2016

Collaborator

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鈥檚 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鈥檛 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鈥檚 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鈥檛 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鈥檚 is similar in that it also asks question, error in what layer. Error in what context, then yes :)
In this case, it鈥檚 error in your 鈥渁pplicative 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

Collaborator

devxoul commented Jun 7, 2016

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鈥檚 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鈥檛 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鈥檚 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鈥檛 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鈥檚 is similar in that it also asks question, error in what layer. Error in what context, then yes :)
In this case, it鈥檚 error in your 鈥渁pplicative 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

@devxoul devxoul closed this Jun 7, 2016

This was referenced Mar 30, 2017

@amirpervaiz086

This comment has been minimized.

Show comment
Hide comment
@amirpervaiz086

amirpervaiz086 Nov 17, 2017

@devxoul why not we just catch error in "catchError" and use error observable in our ViewModel and subscribe that to view controller so when ever error occurs, it will fall into catch and emit error object to view controller. does it make sense?

@devxoul why not we just catch error in "catchError" and use error observable in our ViewModel and subscribe that to view controller so when ever error occurs, it will fall into catch and emit error object to view controller. does it make sense?

@devxoul

This comment has been minimized.

Show comment
Hide comment
@devxoul

devxoul Nov 17, 2017

Collaborator

@amirpervaiz086, Yeah that can be another solution. But I think I wanted to do that with a single observable at that time :)

Collaborator

devxoul commented Nov 17, 2017

@amirpervaiz086, Yeah that can be another solution. But I think I wanted to do that with a single observable at that time :)

@wongzigii

This comment has been minimized.

Show comment
Hide comment
@wongzigii

wongzigii Dec 7, 2017

@devxoul Let's say I finally got an Observable<Result<User>> from network request, How can I unwrap this into Observable<User> or Driver<User> ?

@devxoul Let's say I finally got an Observable<Result<User>> from network request, How can I unwrap this into Observable<User> or Driver<User> ?

@devxoul

This comment has been minimized.

Show comment
Hide comment
@devxoul

devxoul Dec 7, 2017

Collaborator

@wongzigii you can use pattern matching.

Collaborator

devxoul commented Dec 7, 2017

@wongzigii you can use pattern matching.

@wongzigii

This comment has been minimized.

Show comment
Hide comment

@devxoul I finally found dematerialize

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