RxSwift provides a variety of different error-handling operators that can be combined in chain operations to achieve complex processing logic. Here is a brief overview of the error-handling operations provided by RxSwift, followed by some concrete examples to see how they can be used in real projects. RxSwift will not be covered in detail here, but you need to understand the basics of Rx before reading.

Error handling operator

throw

Closure signatures in many Rx operators have a throws modifier, such as the signature of a map method:

func map<R>(_ transform: @escaping (E) throws -> R) - >Observable<R>
Copy the code

We can throw an error in such an operation, which is passed down the chain, as in the following code:

Observable.of(3.2.1) // Create an Observable with three events
    .map { (n) -> Int in
        if n < 2 {
            throw CustomError.tooSmall // 1. Throw a custom error
        } else {
            return n * 2
        }
    }
	.subscribe { event in
		// 2. An error of type CustomError. TooSmall is received}...Copy the code

When the number is less than 2, throw a custom error type in the map, which is passed to the following subscribe.

catchError

RxSwift can catch errors in a chain operation, either by an Observable or by a user throw, and catch errors in a catchError.

Observable.of(3.2.1).map{... } .catchError({ (error) ->Observable<Int> in
        if case CustomError.tooSmall = error {
            return .just(2) // 1. Returns a 2 after catching the tooSmall error
        }
        return .error(error) // 2. Other errors are not handled and continue to be passed along the chain
    })
    .subscribe { event in
		// 3. When tooSmall error occurs, 2 is received and the final result is 3, 2, 2}...Copy the code

This approach is close to the language’s own try… Catch mechanism, very convenient to use.

retry

Retry provides error retries, adding retry to an Observable so that subscribers do not receive error events when the Observable fails. This usually means that the Observable creates event-related operations again. For example, if this is a network request-related Observable, “re-subscribe” resends the related network requests:

moyaProvider.rx.request(.customData)
    .retry() // 1. Retry the request if an error occurs
    .subscribe { 
    	// 2. The retry process is invisible to the subscriber, and events are received only after the request is successful
    }
Copy the code

The Retry method can also take a parameter of type Int, indicating the maximum number of retries.

retryWhen

RetryWhen is conditional retry, which can be retried only under certain conditions. The retryWhen method has a special signature. The parameter it accepts in its closure is not a simple Error type, but an Observable

.

Observable.of(3.2.1).map{... }Observable<()> // 1. RetryWhen: Observable<()
    .retryWhen({ (errorObservable) -> Observable< > ()in
       	// 2. Convert it to another type of Observable with flatMap
        return errorObservable.flatMap({ (error) -> Observable< > ()in
            if case CustomError.tooSmall = error {
                return .just(()) // 3. Return a next event indicating a retry
            }
            return .error(error) // 4. An error message is returned indicating that no operation is performed})})Copy the code

The Observable returned by the closure can be of any type. Because retryWhen only cares about the event in the Observable, not the data type it hosts, it simply uses an empty type and returns an Observable with a.next event if you need to try again.

One advantage of retryWhen’s design is that it can associate its retry logic with another Observable event stream in the event of an error (I’ll show you an example later). But in a simple scenario like the one above, it would be too cumbersome to use. Here we can do a simple wrapper that provides a closure of type (Error) -> Bool to handle the judgment logic:

extension ObservableType {
    public func retryWhen<Error: Swift.Error>(_ shouldRetry: @escaping (Error) -> Bool) - >Observable<E> {
        return self.retryWhen({ (errorObserver: Observable<Error- > >)Observable< > ()in
            return errorObserver.flatMap({ (error) -> Observable< > ()in
                if shouldRetry(error) {
                    return .just(())
                }
                return .error(error)
            })
        })
    }
    
    public func retryWhen(_ shouldRetry: @escaping (Swift.Error) -> Bool) - >Observable<E> {
        return self.retryWhen({ (errorObserver: Observable<Swift.Error- > >)Observable< > ()in
            return errorObserver.flatMap({ (error) -> Observable< > ()in
                if shouldRetry(error) {
                    return .just(())
                }
                return .error(error)
            })
        })
    }
}
Copy the code

Copy the above code into your project and the previous retry logic becomes:

. .retryWhen({ (error) ->Bool in
    if case CustomError.tooSmall = error {
        return true
    }
    return false})...Copy the code

It looks much clearer and relieves the mental burden.

The practical application

Moya is a network library commonly used by Swift, which provides the interface of Rx. The following example uses Moya as a network library to demonstrate, one of Moya’s core protocols is TargetType. If you do not know Moya, you can take a look at its documentation. Let’s look at two common practical application scenarios

Scenario 1: Retry with an interactive error

In many cases, when a user’s operation fails, it cannot be retried directly. Instead, it is given to the user to decide what to do next. For example, there is a file download request, when the download fails, a pop-up box is required to ask whether to retry. That is, there is a ** “break” ** between the error and the retry, and the chain continues only after the user has made a choice.

The solution is to use retryWhen, which associates the Observable

in the parameter with the Observable of our own business logic.

First, we assume that we have a control with such a confirmation box that has the following signature:

class ConfirmView: UIView {
    /// Display a confirmation box in the view, callback is the clicked callback, click confirm callback true, click cancel callback false
    static func show(_ title: String, _ callback: (Bool) -> Void) {... }}Copy the code

There are often many encapsulated control types in real projects, and with the extension mechanism provided in RxSwift, it only takes a small extension to seamlessly integrate into the world of Rx:

extension Reactive where Base: ConfirmView {
    Observable
      
        < // Observable
       
         < // Observable
        
       
      
	static func show(_ title: String) -> Observable<Bool> {
        // Create an Observable
      
        return Observable<Bool>.create({ (observer) -> Disposable in
            // 3. Call the original show method and send the result via observer in a callback
            ConfirmView.show(title, { (confirm) in
                observer.onNext(confirm)
                observer.onCompleted()
            })
            return Disposables.create { 
            	// do some cleanup}}}})Copy the code

This method can then be called as confirmView.rx.show (XXX), which pops up a selection box waiting for the user to select. The result of the selection is notified by an Observable event. We then use flatMap to associate this Observable with Obverable

in retryWhen:

. .retryWhen({ (errorO) ->Observable< > ()in
    return errorO.flatMap({ (error) -> Observable< > ()in
        if case CustomError.tooSmall = error {
            return ConfirmView.rx
                .show("Do you want to retry?").map {
                    if $0 { If retry is selected, next() indicates retry
                        return()}else {
                        throw error // 2. Otherwise, error is returned to pass the error down}}}return .error(error)
    })
})
.subscribe {
	// 3. If retry is selected, no error events will be received}...Copy the code

Similarly, different operations are encapsulated into simple logic flow like Observable, and then combined with operations provided by RxSwift to achieve more complex logic, which is also the functional idea advocated by Rx.

Scenario 2:401 Authentication

401 error is one of the most common application scenarios, such as certification in our application process is as follows: when the server need to authenticate the user login information will return a status code of 401, when the client will authentication information added to the request and send the current request, a process that the business side of the upper should be no perception.

This is a bit different from the previous example: Instead of trying to retry the entire request, we need to modify the original request and add a custom Header. The simplest and most crude way is to send a notification when the 401 error is detected and add the Header to the request Header when it is received:

moyaProvider.request(target)
    .map({ (response) -> Response in
        if response.statusCode == 401 { // Convert 401 to a custom error type
            // Send notification first and retry later
            NotificationCenter.default.post(name: .AddAuthHeader, object: nil)
            throw NetworkError.needAuth
        } else {
            return response
        }
    })
    .retry()
Copy the code

This is not a good idea because Rx focuses on the flow of events, interrupting what should be a coherent logic of notifications. When we get to this point, we have to stop and search the name of the notification globally to find the location of the response, which is not conducive to reading and runs counter to the philosophy of Rx.

The approach I have taken here is not to retry when an error is caught, but to return a new network request. To make this new network request work seamlessly with the previous logic, you first need to define a proxy TargetType:

let ProxyProvider = NetworkProvider<ProxyTarget> ()enum ProxyTarget {
    / / add the Header
    case addHeader(target: TargetType, headers: [String: String]) 
    // ...
}

extension ProxyTarget: TargetType {
	var headers: [String: String]? {
        switch self {
        // 1. Add the new Header to the proxied Target
        case let .addHeader(target: target, headers: headers):
            return headers.merging(target.headers ?? [:], uniquingKeysWith: { (first, second) -> String in
                return first
            })
        }
    }
    
    // 2. Returns properties of the propped Target directly where no blow is required
    var task: Task {
        switch self {
        case let .addHeader(target: target, headers: _) :return target.task
        }
    }
    
    // ...
}
Copy the code

Instead of defining a new network request, the ProxyTarget is used to proxy another TargetType. Here we only define an addHeader action to modify the request’s Header.

The final implementation is as follows:

provider.request(target)
    .map({ (response) -> Response in
        if response.statusCode == 401 { // 1. Convert 401 to a custom error type
            throw NetworkError.needAuth
        } else {
            return response
        }
    })
    .catchError({ (error) -> Single<Response> in
        if case NetworkError.needAuth(let response) = error{
            // 2. Catch the unauthenticated error, add the authentication header and try again
            let authHeader = ... // Calculate the authentication header
            let target = ProxyTarget.addHeader(target: token, headers: authHeader)
            return ProxyProvider.rx.request(target, callbackQueue: callbackQueue)
        }
        return Single.error(error)
    })
    .subscribe {
        // 3. The authentication process is insensitive to upper-layer business parties}...Copy the code

Use map to convert 401 to a custom error type, catch the error in catchError, and return a new Observable using a ProxyTarget with an authentication header, so all the associated logic is clustered in a chain of calls.

Of course, in a real project there may be many other types of business related errors as well as 401 errors, and it is not a good idea to deal with all of them in a map. It is best to isolate this logic and put it in the Moya Plugin, which is not demonstrated here.

The last

The abstraction of event flow in Rx is very powerful and can be used to describe all kinds of complex scenarios. Here are just a few simple examples from the error handling aspect, and you can see that the thinking of Rx is very different from that of normal code, and the shift in thinking is the key to understanding Rx.