RxSwift: 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
}
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?
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 3
- Comments: 15 (8 by maintainers)
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 ofObservable<User>
from API function and treat API error asResult.Failure
.API.swift
LoginViewController.swift
LoginViewModel.swift
I attach the whole conversation for others 😄
There was a comment here by @kean which was deleted. Went as follows:
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
andSignal
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 usingmaterialize()
on a regularObservable<[String: Any]>
and splitting errors and elements. Me and my team personally prefer the latter as it provides more control (and basically anEvent
has the same shape of aResult
, minus the typed error).Hope this helps clear up some of the ideas (my personal thoughts, at least) about how to leverage error handling.
Since RxSwift doesn’t use typed errors, we have no way to know “you handle the error upstream”. As far as Rx is concerned, whatever you feed into the Driver may emit an error.
Overloads around
Result
, etc, are opinionated, and not specifically in the scope of RxSwift IMO.You could easily create your own overloads around your specific use-cases:
For example, I personally use this in cases I’m absolutely positive I’ve handled the error:
You can also deal with Result, specifically, in a similar way:
In regards to where you place error handling, obviously scoping is important 😃 This is true in RxSwift like it is in Combine, and like it is even in Swift (where the position of the do/catch obviously matters).
Eventually traits are a means to let you communicate to outside consumers that you provide some guarantees. If you are absolutely sure you’ve dealt with the error, you can use one of the above syntactic sugar options to “silence” failures.
You might want to instead have a regular
asDriver()
that also fataLErrors in DEBUG, just to catch mistakes early on:@devxoul I finally found dematerialize
@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?