Rxswift Error Handle Issue

RXSwift: Catch an error without emiting an element from the stream

If you don't want to bring in the whole RxSwiftExt library for just one thing you can use .catchError { _ in Observable.empty() } which will eat the error and emit a complete event.

Another option is .catchError { _ in Observable.never() } which will eat the error and then emit nothing.

rxswift error handle issue

I suggest to make the type of createObservable PublishSubject<Observable<PassbookModelType>>, instead of BehaviorSubject<PassbookModelType?> which, I guess, accidentally flattens two Rx streams conceptually separatable each other: the saveObject process itself (an one-shot process) and starting the saveObject process initiated by user action repeatedly. I've written a short example to demonstrate it.

let createObservable = PublishSubject<Observable<Int>>()

override func viewDidLoad() {
super.viewDidLoad()
createObservable.flatMap {
$0.map { obj in
print("success: \(obj)")
}
.catchError { err in
print("failure: \(err)")
return empty()
}
}.subscribe()
}

// Simulates an asynchronous proccess to succeed.
@IBAction func testSuccess(sender: UIView!) {
let oneShot = PublishSubject<Int>()
createObservable.onNext(oneShot)
callbackAfter3sec { res in
oneShot.onNext(1)
oneShot.onCompleted()
}
}

// Simulates an asynchronous process to fail.
@IBAction func testFailure(sender: UIView!) {
let oneShot = PublishSubject<Int>()
createObservable.onNext(oneShot)
callbackAfter3sec { res in
oneShot.onError(NSError(domain: "Error", code: 1, userInfo: nil))
}
}

func callbackAfter3sec(completion: Int -> ()) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC * 3)), dispatch_get_main_queue()) {
completion(2)
}
}

There is an important merit with that: If the one-shot process would become in the Rx style (for example, like as callbackAfter3sec() -> Observable<Int>) in the future, there were no need to re-write the use-side code like in the viewDidLoad above. There is an only one change to do is to pass an Observable<> object to createObservable.onNext(...).

Sorry for my poor English skill. I hope this makes sense to you.

How to handle error from api request properly with RxSwift in MVVM?

You can do that by materializing the even sequence:

First step: Make use of .rx extension on URLSession.shared in your network call

func networkCall(...) -> Observable<[Post]> {
var request: URLRequest = URLRequest(url: ...)
request.httpMethod = "..."
request.httpBody = ...

URLSession.shared.rx.response(request)
.map { (response, data) -> [Post] in
guard let json = try? JSONSerialization.jsonObject(with: data, options: []),
let jsonDictionary = json as? [[String: Any]]
else { throw ... } // Throw some error here

// Decode this dictionary and initialize your array of posts here
...
return posts
}
}

Second step, materializing your observable sequence

viewModel.networkCall(...)
.materialize()
.subscribe(onNext: { event in
switch event {
case .error(let error):
// Do something with error
break
case .next(let posts):
// Do something with posts
break
default: break
}
})
.disposed(by: disposeBag)

This way, your observable sequence will never be terminated even when you throw an error inside your network call, because .error events get transformed into .next events but with a state of .error.

Handling error with RxSwift and MVVM

The use of Variables is throwing you off I think. Avoid them when possible.

My impression of this code:

  • When the view model is created, it makes a call getObservable().

  • There is no provision for input from the user.

  • The showError variable seems to be getting used as a trigger rather than to actually pass a value so it should be Void instead of Bool

I would expect this view model to look more like:

struct MapViewModel {
let list: Observable<[MyObject]>
let showError: Observable<Void>

init() {
let dataResult = getObservable()
.materialize()
.shareReplayLatestWhileConnected()
list = dataResult.map { $0.element }
.filter { $0 != nil }
.map { $0! }
showError = dataResult.map { $0.error }
.filter { $0 != nil }
.map { _ in }
}
}

The secret sauce in the above is the use of materialize to convert the error into an onNext event so you can use it for the trigger.

The subscribes/binds and dispose bag should be in the view controller, not in the view model.

How to get Error from network request using RxSwift

Your view model is pretty strong, just a few slight errors. The fact that your network request returns an Observable Result means you need to use compact map and a case let to extract the values. There are libraries that make this easier. One is called RxEnumKit.

class SearchViewModel {

// MARK: Properties
let manager: NetworkManager

// MARK: Binding
struct Input {
let searchText: Observable<String>
let validate: Observable<Void>
}

struct Output {
let followers: Driver<[FollowerViewModel]> // note, this should not return a Driver-result.
let errorMessage: Driver<String>
}

init(manager: NetworkManager) {
self.manager = manager
}

func transform(input: Input) -> Output {
let followers = input.validate
.withLatestFrom(input.searchText)
.filter { !$0.isEmpty }
.flatMapLatest { query in
return self.manager.getFollowers(with: query, page: 1)
}

let missingName = input.validate
.withLatestFrom(input.searchText)
.compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }

// the below extracts the error string from the followers observable and merges it with the missingName observable to make the errorMessage observable.
let errorMessage = Observable.merge(
missingName,
followers.compactMap { (result) -> String? in
guard case let .failure(error) = result else { return nil }
return error.localizedDescription
}
)
.asDriver(onErrorJustReturn: "")

// use the same thing here to pull the followers out of the Result.
let followerVM = followers.compactMap { (result) -> [FollowerViewModel]? in
guard case let .success(followers) = result else { return nil }
return followers.map { FollowerViewModel(follower: $0) }
}
.asDriver(onErrorJustReturn: [])

return Output(followers: followerVM, errorMessage: errorMessage)
}
}

What is the best practice to deal with RxSwift retry and error handling

I read some post says that the best practice to deal with RxSwift is to only pass fatal error to the onError and pass Result to the onNext.

I don't agree with that sentiment. It is basically saying that you should only use onError if the programmer made a mistake. You should use errors for un-happy paths or to abort a procedure. They are just like throwing except in an async way.

Here's your algorithm as an Rx chain.

enum ReceiptError: Error {
case noReceipt
case tooManyAttempts
}

struct Response {
// the server response info
}

func getReceiptResonse() -> Observable<Response> {
return fetchReceiptLocally()
.catchError { _ in askAppleForReceipt() }
.flatMapLatest { data in
sendReceiptToServer(data)
}
.retryWhen { error in
error
.scan(0) { attempts, error in
let max = 1
guard attempts < max else { throw ReceiptError.tooManyAttempts }
guard isRetryable(error) else { throw error }
return attempts + 1
}
}
}

Here are the support functions that the above uses:

func fetchReceiptLocally() -> Observable<Data> {
// return the local receipt data or call `onError`
}

func sendReceiptToServer(_ data: Data) -> Observable<Response> {
// send the receipt data or `onError` if the server failed to receive or process it correctly.
}

func isRetryable(_ error: Error) -> Bool {
// is this error the kind that can be retried?
}

func askAppleForReceipt() -> Observable<Data> {
return Observable.just(Bundle.main.appStoreReceiptURL)
.map { (url) -> URL in
guard let url = url else { throw ReceiptError.noReceipt }
return url
}
.observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.map { try Data(contentsOf: $0) }
}

RXSwift chaining observers and catch errors

Well, this is a little cleaner:

func updateEvent(_ event: EventModel, _ group: GroupModel, newMembers: [MemberModel], removeMembers: [AttendanceModel]) {

let invitedResults = newMembers
.compactMap { [repository] member in
repository?.inviteToEvent(member: member, event: event, group: group)
}

let removedResults = removeMembers
.compactMap { [repository] member in
repository?.removeFromEvent(member: member, event: event, group: group)
}

Observable.zip(invitedResults + removedResults)
.flatMap { [repository] _ in repository?.updateEvent(group, event) ?? .empty() }
.subscribe(
onNext: { [presenter] event in
presenter?.eventSuccessfullyUpdated(event)
},
onError: { [presenter] error in
presenter?.failedWithError(error)
}
)
.disposed(by: disposeBag)
}

It avoids capturing self and making a retain cycle at least. But if any of the invites or removes error it could leave your system in an invalid state...

why I can't keep observable alive after .onError() even I already use error handling like .catchError() onRxSwift?

Once you push an error into your inputData it is done and will no longer emit anything. (full stop) That is the Observable contract and catching the error from a different Observable doesn't change that.

Learn more about the Observable Contract which says in part:

Upon issuing an OnCompleted or OnError notification, it may not thereafter issue any further notifications.

[stress is mine]


As I mentioned in the comments: That observable will no longer be able to emit anything and any subscriptions on it will be disposed. However, the only subscription on it is from inside the flatMap. Your data subject will still be able to emit more.

for example:

let bag = DisposeBag()
let data = PublishSubject<Observable<Int>>()

data
.debug("debug")
.flatMap {
$0.catchError { _ in Observable.empty() }
}
.subscribe(
onNext: { print($0) },
onCompleted: { print("end") }
)
.disposed(by: bag)

let inputData1 = PublishSubject<Int>()

data.onNext(inputData1)
inputData1.onNext(1)
inputData1.onNext(2)
inputData1.onError(MyError.anError)

let inputData2 = PublishSubject<Int>()
data.onNext(inputData2)
inputData2.onNext(2)

Will print out:

debug -> subscribed
debug -> Event next(RxSwift.PublishSubject<Swift.Int>)
1
2
debug -> Event next(RxSwift.PublishSubject<Swift.Int>)
2

RxSwift errors dispose of subscriptions

At the place where you make the login call:

let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
}
.share()

You can do one of two things. Map the login to a Result<T> type.

let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.map(Result<LoginResponse>.success)
.catchError { Observable.just(Result<LoginResponse>.failure($0)) }
}
.share()

Or you can use the materialize operator.

let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.materialize()
}
.share()

Either method changes the type of your loginResponse object by wrapping it in an enum (either a Result<T> or an Event<T>. You can then deal with errors differently than you do with legitimate results without breaking the Observable chain and without loosing the Error.

Another option, as you have discovered is to change the type of loginResponse to an optional but then you loose the error object.



Related Topics



Leave a reply



Submit