Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

14. Error Handling in Practice
Written by Florent Pillet

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Life would be great if we lived in a perfect world, but unfortunately things frequently don’t go as expected. Even the best RxSwift developers can’t avoid encountering errors, so they need to know how to deal with them gracefully and efficiently. In this chapter, you’ll learn how to deal with errors, how to manage error recovery through retries, or just surrender yourself to the universe and let the errors go.

Getting started

This application is a continuation of the one you worked on in Chapter 12, “Beginning RxCocoa”. In this version of the application, you can retrieve the user’s current position and look up weather for that position, but also request a city name and see the weather in that locaiton. The app also has an activity indicator to give the user some visual feedback.

Before continuing, make sure you have a valid OpenWeatherMap API Key. If you don’t already have a key, you can sign up for one at:

Once you’ve completed the sign-up process, visit the dedicated page for API keys and generate a new one at:

Once that’s done, use Terminal to navigate to the root of the project and perform the necessary pod install.

Once the pods have been installed, open ApiController.swift, take the key you generated above and replace the placeholder in the following location:

let apiKey = BehaviorSubject(value: "Your Key")

Run the app and make sure that the application compiles and that you can retrieve the weather when you search for a city.

If that all looks good, then you can proceed right into the next section!

Managing errors

Errors are an inevitable part of any application. Unfortunately, no one can guarantee an application will never error out, so you will always need some type of error-handling mechanism.

Throwing errors

A good place to start is by handling RxCocoa errors, which wrap the system errors returned by the underlying Apple frameworks. RxCocoa errors provide more details on the kind of error you’ve encountered, and also make your error handling code easier to write.

public func data(request: URLRequest) -> Observable<Data> {...}
if 200 ..< 300 ~= pair.0.statusCode {
  return pair.1
} else {
  throw RxCocoaURLError.httpRequestFailed(response: pair.0, data: pair.1)
}

Handle errors with catch

After explaining how to throw errors, it’s time to see how to handle errors. The most basic way is to use catch. The catch operator works much like the do-try-catch flow in plain Swift. An observable is performed, and if something goes wrong, you return an event that wraps an error.

func catchError(_ handler:) -> RxSwift.Observable<Self.E>

func catchErrorJustReturn(_ element:) -> RxSwift.Observable<Self.E>

A common pitfall

Errors are propagated through the observables chain, so an error that happens at the beginning of an observable chain will be forwarded to the final subscription if there aren’t any handling operators in place.

"http://api.openweathermap.org/data/2.5/weather?q=goierjgioerjgioej&appid=[API-KEY]&units=metric" -i -v
Failure (207ms): Status 404

Catching errors

Now that you’ve covered some theory, you can move on to writing code and updating the current project. Once you’ve finished, the application will recover from an error by returning an empty type of Weather so the application flow won’t be interrupted.

private var cache = [String: Weather]()
let textSearch = searchInput.flatMap { text in
  return ApiController.shared.currentWeather(city: text)
    .do(onNext: { [weak self] data in  
      self?.cache[text] = data
    })
    .catchErrorJustReturn(.empty)
}
.catchError { error in
  return Observable.just(self?.cache[text] ?? .empty)
}

Retrying on error

Catching an error is just one way errors are handled in RxSwift. You can also handle errors with retry. When a retry operator is used and an observable errors out, the observable will repeat itself. It’s important to remember that retry means repeating the entire task inside the observable.

Retry operators

There are three types of retry operators. The first one is the most basic:

func retry() -> Observable<Element>
//.catchError { error in
//  return Observable.just(self?.cache[text] ?? .empty)
//}
func retry(_ maxAttemptCount:) -> Observable<Element>
let textSearch = searchInput.flatMap { text in
  return ApiController.shared.currentWeather(city: text)
    .do(onNext: { [weak self] data in
      self?.cache[text] = data
    })
    .retry(3)
    .catchError { [weak self] error in
      return Observable.just(self?.cache[text] ?? .empty)
    }
}

Advanced retries

The last operator, retryWhen, is suited for advanced retry situations. This error handling operator is considered one of the most powerful:

func retryWhen(_ notificationHandler:) -> Observable<Element>
subscription -> error
delay and retry after 1 second

subscription -> error
delay and retry after 3 seconds

subscription -> error
delay and retry after 5 seconds

subscription -> error
delay and retry after 10 seconds
let maxAttempts = 4
.retryWhen { e in
  // flatMap source errors
}
return e.enumerated().flatMap { attempt, error -> Observable<Int> in
  // attempt few times
}
.retryWhen { e in
  return e.enumerated().flatMap { attempt, error -> Observable<Int> in
    if attempt >= maxAttempts - 1 {
      return Observable.error(error)
    }
    return Observable<Int>.timer(.seconds(attempt + 1),
                                 scheduler: MainScheduler.instance)
                          .take(1)
  }
}
print("== retrying after \(attempt + 1) seconds ==")
== retrying after 1 seconds ==
... network ...
== retrying after 2 seconds ==
... network ...
== retrying after 3 seconds ==
... network ...

Custom errors

Creating custom errors follows the general Swift principle, so there’s nothing here that a good Swift programmer wouldn’t already know, but it’s still good to see how to handle errors and create tailored operators.

Creating custom errors

The errors returned from RxCocoa are quite general, so an HTTP 404 error (page not found) is pretty much treated like a 502 (bad gateway). These are two completely different errors, so it would be good to be able to handle them differently.

enum ApiError: Error {
  case cityNotFound
  case serverFailure
}
return session.rx.response(request: request)
  .map { response, data in
  switch response.statusCode {
  case 200 ..< 300:
    return data
  case 400 ..< 500:
    throw ApiError.cityNotFound
  default:
    throw ApiError.serverFailure
  }
}

Using custom errors

Now that you’re returning your custom error, you can do something constructive with it.

InfoView.showIn(viewController: self, message: "An error occurred")
.do(onNext: { [weak self] data in
  self?.cache[text] = data  
})
.do(
  onNext: { [weak self] data in
    self?.cache[text] = data
  },
  onError: { error in
    DispatchQueue.main.async { [weak self] in
      guard let self = self else { return }
      InfoView.showIn(viewController: self, message: "An error occurred")
    }
  }
)

private func showError(error e: Error) {
  guard let e = e as? ApiController.ApiError else {
    InfoView.showIn(viewController: self, message: "An error occurred")
    return
  }

  switch e {
  case .cityNotFound:
    InfoView.showIn(viewController: self, message: "City Name is invalid")
  case .serverFailure:
    InfoView.showIn(viewController: self, message: "Server error")
  }
}
self.showError(error: error)

Advanced error handling

Advanced error cases can be tricky to implement. There’s no general rule about what to do when the API returns an error, besides show a message to the user.

case invalidKey
case 401:
  throw ApiError.invalidKey
case .invalidKey:
  InfoView.showIn(viewController: self, message: "Key is invalid")
let retryHandler: (Observable<Error>) -> Observable<Int> = { e in
  return e.enumerated().flatMap { attempt, error -> Observable<Int> in
      // error handling
  }
}
if attempt >= maxAttempts - 1 {
  return Observable.error(error)
} else if let casted = error as? ApiController.ApiError, casted == .invalidKey {
  return ApiController.shared.apiKey
    .filter { !$0.isEmpty }
    .map { _ in 1 }
}
print("== retrying after \(attempt + 1) seconds ==")
return Observable<Int>.timer(.seconds(attempt + 1),
                             scheduler: MainScheduler.instance)
                      .take(1)
.retryWhen(retryHandler)
let apiKey = BehaviorSubject(value: "")

Materialize and dematerialize

Error handling can be be a difficult task to achieve, and sometimes it’s necessary to debug a sequence which is failing by decomposing it to better understand the flow. Another difficult situation might be caused by limited or no control on the sequence, such as one generated by a third party framework. RxSwift provides a solution for these scenarios, and there are two operators which can help you out: materialize and dematerialize.

observableToLog.materialize()
  .do(onNext: { (event) in
    myAdvancedLogEvent(event)
  })
  .dematerialize()

Challenge

Challenge: Use retryWhen on restored connectivity

In this challenge you need to handle the condition of an unavailable internet connection.

_ = RxReachability.shared.startMonitor("openweathermap.org")

Where to go from here?

In this chapter, you were introduced to error handling using retry and catch. The way you handle errors in your app really depends on what kind of project you’re building. When handling errors, design and architecture come in play, and creating the wrong handling strategy might compromise your project and result in re-writing portions of your code.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now