How To Error Out in RxSwift
Generally there are 3 ways to terminate current sequence with an error.
Create a special observable that emits nothing but an error
Observable.error(someError)
.class LocationService { enum Error: Swift.Error { ... case serviceDisabled ... } func startLocationUpdating() -> Observable<Location> { guard CLLocationManager.locationServiceEnabled else { return .error(Error.serviceDisabled) } ... } }
Emit an error event to the observer parameter in an
Observable<T>.create
block parameterobserver.onError(someError)
.Observable<T>.create { observer in ... let someError = ... observer.onError(someError) ... return Disposables... }
Throw an error in those operator block parameters that are defined as
throws
, the sequence would terminates on the error event.someSequence.map { value in guard ... else { let someError = ... throw someError } ... }
Error Handling Policies
RxSwift provides 2 handling policies:
Catch error and switch to another sequence
Retry the original sequence
Actually the retry way is just a specialized version of the catch way - it catch errors and then switch to same sequence again.
Strategy #1 - Catch Errors
There are 4 error catching operators I known:
On error switch to another sequence
seq.catchError { error -> Observable<T> in .... }
The block, on error, would return a new observable sequence to switch to.
On error end with a given value
seq.catchErrorJustReturn(someValue)
It is equivalent to
seq.catchError { _ in return .just(someValue) }
On error just complete silently
seq.catchErrorJustComplete()
It is equivalent to
seq.catchError { _ in return .empty() }
, this operator is provided by the RxSwiftExt community project.On error switch to next observable
Observable<T>.catchError(swiftSequenceOfObservables)
The traits Driver
and Signal
from RxCocoa also provides similar operators
when converting from ordinary sequences:
seq.asDriver(onErrorJustReturn: someValue)
seq.asDriver(onErrorDriveWith: alternativeDriver)
seq.asDriver(onErrorRecover: { error -> Driver<T> in ... })
seq.asSignal(onErrorJustReturn: someValue)
seq.asSignal(onErrorSignalWith: alternativeSignal)
seq.asSignal(onErrorRecover: { error -> Signal<T> in ... })
Sequence error catching is really useful for those flatMap
scenarios, where
the outer sequence would terminate if any of its inner sequence errors out
(inner errors would be propagated out and terminate outer sequence).
seq
.flatMap { value -> Observable<T> in
return makeInnerSequence()
.catchError { error in
jack.error("inner error: \(error)")
return alternativeSequence
}
// or
.catchErrorJustReturn(someValue)
// or
.catchErrorJustComplete()
// or
.asDriver(onError...)
}
.asDriver(onError...)
Strategy #2 - Retry
RxSwift provides 3 retry operators:
Retry unlimited
seq.retry()
retry unconditionally, use it with caution.Retry limited times
seq.retry(count)
retry at mostcount
times then errors out.Retry conditionally
seq.retryWhen { errorObservable -> TriggerObservable in ... }
This is the most powerful retry policy. For each stripe of errors & retries, the operator create a new error observable which emits current stripe of consecutive errors and pass it into the block paramter. The block paramter, according to the error sequence, return an appropriate triggering observable, the operator wait only the first element from the trigger observable then retry the source sequence.
Usage #1 - Retry after incremental backoff delay
seq.retryWhen { errorObservable -> Observable<T> in return errorObservable .enumerated() .flatMap({ index, error in if index < 3 { let delay = pow(2, (index + 1)) return Observable<T>.timer(delay, scheduler: MainScheduler.instance) } else { throw error } }) }
Usage #2 - Retry after pre-condition met, like waiting till the network gets re-connected, authentication passed, permission granted.
seq.retryWhen { errorObservable -> Observable<T> in return errorObservable .flatMap({ error in switch error { case .network: return ReachabilityService.reachByWiFi() case .unauthorized: return AuthenticationService.authenticate() case ... ... } }) }
Notes:
If the observable is not Single
like (i.e. emits more than one .next
events), retry would cause duplicated events emitted again and again.
Especially when work with startWith
or concat
operator, apply them after
the retry
would usually be a better idea.