Building Breather (Part 4 — Bonus): Smooth API error handling with Moya & RxSwift custom operators

Breather is an open-source iOS app that shows the weather, air pollution and asthma conditions around you.

Alexandros Baramilis
7 min readMay 10, 2019

To raise awareness about air pollution and practice my iOS skills at the same time, I’m building an open-source app where I apply some of the latest knowledge I gain about iOS, Swift and programming. Since there is a lot of information to digest, I’m splitting it into small manageable parts:

Smooth API error handling with Moya & RxSwift custom operators.

When we left off in Part 4, we had our two RxMoya requests, we were filtering the responses using RxMoya’s filterSuccessfulStatusCodes() — which only lets through status codes in the 200–299 range — and mapping the successful Moya Response to our custom response structs using RxMoya’s map(type: Decodable.Protocol) — which takes any type conforming to Swift’s Decodable protocol as a parameter and maps the Moya Response to that type.

Our code in Part 4

That was easy to setup and worked nicely, but handled all API errors with a generic MoyaError message: “Status code didn’t fall within the given range.”

It’s really cool if you want to build something fast and don’t care about specific error messages, but what if you do? Just replace filterSuccessfulStatusCodes() with catchAirVisualError() and catchPropellerError().

Our code in Part 4 (Bonus)

And, that’s it! See you next time!

Keep reading → Part 5: Coming soon…

Lol, just kidding.

I mean, the code is real, but how did we get there? Let’s break it down…

Deconstructing filterSuccessfulStatusCodes()

If you want to understand how something works in Xcode, you start tapping on it while holding down command and select Jump to Definition…, and repeat until you get the picture. You can also hold down option when you tap on Jump to Definition… if you want to open the code in the Assistant editor.

By doing that a few times, I discovered that RxMoya’s filterSuccessfulStatusCodes() essentially works like this:

The code speaks for itself here, I don’t think it needs more explaining. But now that we have it, we can begin customising so we can handle errors more specifically.

The error response models

Let’s have a look at what an error response from AirVisual API looks like.

AirVisual API error response

They generally follow this structure, with an error message encapsulated within the data field.

Like we did in Part 4 for successful responses, we can create an AirVisualErrorResponse model struct for parsing error responses.

Throwing custom errors

Now we can get rid of the generic MoyaError and replace it with a custom AirVisualError after we have mapped the response to the AirVisualErrorResponse type.

This do-try-catch handles both decoding errors and API errors in one Swift stroke 😊

I made a custom AirVisualError type for AirVisual API errors (or at least the ones they have in their documentation). The init method inside is what allows us to initialise the enum with the message that we get from the response. For empty messages we have an unknown case and for all other messages that are not handled a with(message: String) case.

I also made an extension of AirVisualError conforming to LocalizedError so we can provide a more user-friendly description when we use error.localizedDescription in the alert to the user.

Making custom RxSwift operators

Now that we have everything together, we can abstract away the code into a handy RxSwift custom operator named catchAirVisualError.

The catchAirVisualError custom operator

This works for any sequence of type Single<Response>, which is the same as the return type of the RxMoya request, but the decoding works only for the AirVisualErrorResponse type, which should be common for all AirVisual API errors.

And we use it simply like:

There is a danger here that someone might accidentally use it to handle errors from another API (ex. Propeller), that’s why I named it catchAirVisualError and included the parameter type, where the developer has to explicitly type the type. This is more of a double-reminder to make the developer think about what type we’re dealing with here. It also helps when you’re reading the code, to remind you that we’re mapping this response type inside this operator. However, it will not create any compiler errors if the operator is used in the wrong sequence, which is not ideal. On the plus side, it won’t crash the app either. The worst thing that can happen is that we try to map the Response to the wrong type and get a Decodable error which is handled normally, although we’re supposed to be getting an API error instead. Anyway, if you can think of way to enforce this, please let me know in the comments.

Repeat for Propeller API

Querying the Air by Propeller API we encounter two types of error responses.

One with a message field:

And one with id and description fields:

But thanks to the flexibility of the Decodable protocol, we can handle both response types with one response model struct.

By making the properties Optional, we can safely decode the Response even when those fields are not present. It will just decode the fields that are present and the rest of the properties will be nil. I also didn’t include the id field since I’m not interested in the those codes.

I made a custom PropellerError type, but with only two cases this time — unknown and with(message: String) — since Propeller didn’t provide any documentation for errors and I didn’t want to manually query the API trying to find every error type. Their error messages are much more readable than AirVisual though, so we can simply show the error messages as they are.

Similarly, we make a custom RxSwift operator named catchPropellerError that will catch all the bad status codes from all Propeller API responses and throw a custom error with the appropriate message.

The catchPropellerError custom operator

The difference here (except for the types), is that after we’ve decoded the response, we check the message property first, and if it’s nil, we use the ?? (Nil-Coalescing Operator) operator to check the description property, and if it’s nil too, we use the ?? operator again to pass an empty string as a default value. I didn’t know we could make a ?? train until I wrote this 😄

So the default empty string will trigger the PropellerError.unknown case which will show “Air by Propeller API: An unknown error occurred.” and all other error messages (be it in the message or the description field) will trigger the PropellerError.with(message: String) case which will show “Air by Propeller API: An error occurred with message: “An error message from Propeller.””.

And we can hide all this under a simple catchPropellerError(PropellerErrorResponse.self) operator.

You gotta ❤️ Swift and its power and elegance.

EDIT: More Observable extensions

I was hot from making RxSwift custom operators so I made another two:

And we can use them like:

😄

For real now…

Keep reading → Part 5: Coming soon…

--

--