RxSwift Errors Done Right!
Basic knowledge of RxSwift — types of events, subscribing, binding, basic operators
Understanding RxSwift is a great skill in every iOS developer’s toolset, and is good to learn at some point in one’s career as a software engineer. The approach and paradigms this framework represents have been appreciated by Apple itself, with the release of their Combine framework at WWDC’19. Combine is a similar framework to RxSwift with similar principles that also allows great asynchronous events handling. The fact that Apple made it one of their base libraries for iOS development assured me that learning RxSwift and the approach it represents was a great idea. In this post I would like to write about one of the core aspects of programming using RxSwift, which is error handling. First, we will get an overview of how to work with errors using the most fundamental RxSwift techniques, how to avoid errors when they are unwanted, and also how I approach this topic in my day to day. To fully understand the following post, it would be good have grasped these fundamentals:
- Basic knowledge of Swift
- Basic knowledge of RxSwift — types of events, subscribing, binding, basic operators
- Enum’s associated values
If you’re not familiar with the items listed above, don’t worry! Keep reading, I’ll keep it simple and will include the appropriate documentation references. Ready? Let’s dive in!
2. RxSwift error event
Let’s start with a simple example of a login screen. We want to create a screen where the user enters his email and password and the app tells us whether the provided credentials are correct. Since sending a login request to our API is an asynchronous task, we will model it as an observable. We begin with a very simple UI that looks like this:
Now we want to bind the UI components and trigger the API request by clicking Login. The code should look something like this:
AppError is defined like this:
Since it conforms to the Error protocol, we can throw it or pass to the Rx Error event.
Now let’s test our example by entering various credentials.
Oops. Something’s not right. We saw correct behaviour when entering the correct credentials, then also correct behaviour with invalid credentials. But after entering the correct credentials once again there was no success prompt. What’s wrong here? If you are familiar with how RxSwift event types works, you already know! An error event terminates the sequence so the observable won’t emit any more events. We would have to subscribe to the login observable again to make it work. So, in conclusion, emitting a standard Rx error event is not really useful in cases where errors are perfectly normal and can occur between next events. So when is a plain error fine? Mostly, when we are performing an operation that can only either emit one result or fail (there is a trait for such tasks called Single — more at https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Traits.md#single). For example — pushing another screen to fetch some information and then popping back with a result. But let’s get back to our example and see what can we do to make it work.
3. Catching and avoiding errors
The first thing we can do is to use the catchError operator. This operator intercepts error events and replaces them with next events using closure provided by the argument. Closure takes the error object that was associated with the error event and transforms it to another observable with the same element type. There is also a similar operator, catchErrorJustReturn which only defines strictly what is returned whenever an error event appears. Unfortunately, this approach won’t work in our case — the observable element type is Void so we cannot associate any information about our error with it. I will tell you how to solve this problem in a minute, but since we’re on catch operators, let me show you some simple examples.
In the following, we’re pretending that we are fetching a user’s friends list in a social network application. The app must first check whether the user is currently authorized and only then make an actual request. The sample code would look like this:
As you can see, we are pretending that both operations failed. Can you guess what will be printed? The console log looks like this:
Catched error: authorization
Great! We avoided emitting an error event and returned an empty array instead. Let’s take a look at what happens if authorization is successful:
The neat thing about catching errors is that it will also catch an error from the transformed observable returned from flatMap. So, right now, the following things will get printed:
Catched error: mapping
We can also use catchErrorJustReturn in our example.
Code functionality and console output will be similar as above, with the only difference being that it won’t print the error message, but that’s understandable.
But there is an important thing that you should be aware of when using catchError. Read the following explanation carefully, as it shows that catchError doesn’t work as intuitively as you might think. This part is more difficult to understand thoroughly, so don’t worry if you don’t get it first time round. Take your time to read the flatMap and catchError documentation and think about it. So let’s modify our example a little:
We have removed the authorization step, but added triggering that fetched a list on button tap. Let’s tap once. Okay, it works, we get an onNext event with an empty array which was returned from catchErrorJustReturn, as expected. But after the next click we have a surprise. Nothing gets emitted. What’s going on?
It turns out that the whole sequence completes and it won’t emit any more events — the same as if it has errored. We can verify by changing our code:
So it turns out that catchError also completes the current sequence. It is counterintuitive and makes this operator less practical. Is there anything we can do in this situation to handle errors without a need to re-subscribe? Yes! Take a look at this:
If we try again, we can see that every tap now emits a next event with an empty array. You might be wondering — how’s that possible? The only thing we did was to move catchError inside the flatMap! catchError is supposed to catch every error that shows up before it’s in the pipe, so what’s the difference? Why does the sequence not complete now? It’s not actually that simple. Try updating flatMap like this:
And now, after each time we press the button, the following is getting printed:
onSubscribe flat map
onCompleted flat map
So the whole sequence doesn’t end and you can click as many times as you want and it will still emit events, even though the observable that is returned from flatMap completes! How is that possible? Notice that onSubscribe is also being called whenever we tap the button. Despite subscribing the whole sequence only once, the observable that is created inside the flatMap is subscribed internally inside the flatMap every time this operator is called. So even if it completes, the next time we tap the button it is created again and subscribed that’s why we are getting only onNext events in the main subscription. In conclusion you should be careful about where you put your catchError operator as it might make a big difference. Feel free to experiment yourself, by putting it in different places of your pipe and see what changes.
4. Custom approach
Okay, so now we know how to avoid finishing a sequence with an error, but we still don’t get the error in the subscription closures. Let me show you a great solution for this — introducing hero of the hour Result enum:
This simple type can wrap your operation result and contain information whether it was successful (and contain the value) or it failed (and contains the error). To fully understand this, knowing about enum’s associated value would be helpful. If it’s new to you, check this out for more:
https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html (Associated Values section)
We can now return to our login example and replace observable element from Void to Result<Void>. The updated example should look something like this:
And now everything works as expected! After entering the wrong credentials you can still continue to enter new ones. I didn’t include the catchError operator in this example since that’s a very simple case. You can do this yourself using catchError as your homework!
Let me tell you about my general approach to handling errors. I use the Result enum a lot; whenever I’m doing an asynchronous task that might fail, I like to split the observable sequence into two, a sequence of errors and a sequence of values. This way it is easier to bind them to where they are needed, for example transforming and binding errors to the view layer.
Firstly, we have to alter the Result enum a little. We will write convenience computed properties which will return either an optional value or optional error casted to our AppError. It will also conform to a protocol which I called ResultProtocol, which might look like it’s not doing much. Actually, it is required for something called type erasure and it will allow us to write an extension on ObservableType and constrain it to those observables, which has Result as its element. I don’t want to get deeper into that here though, so for now just treat it as a method that allows writing extensions in this case. If you want to get down and dirty with details on type erasure, check out this great post: https://www.swiftbysundell.com/articles/different-flavors-of-type-erasure-in-swift/
So here’s the code:
Now, it would be great to write a reusable code section that would allow us to easily unwrap and split observables with Result elements. Take a look at this:
Let me break those functions down. The first one maps the Result observable to its observable element type. It checks whether there is a success value associated with the result and returns it, otherwise it returns nil. The Unwrap operator filters out nils and transform observable elements to non-optional, so we can be sure that only actual values will be emitted from this observable. It is available in the RxSwiftExt pod. The bottom function works in a similar manner, but for errors.
Now let’s take a look at how we can use this code in practice. Let’s imagine we have a service that fetches an app user’s list (simplified code):
Now let’s imagine we have a separate class (viewModel, for example) that is using this for fetching users. It would have the following properties:
Notice that we need to share the original observable, so we are not triggering too many API calls when subscribing indirectly from many places. Now we have well-defined sequences for errors and values which can be bound where needed, for example values can be bound to collection view.
One can achieve a similar result when using the materialize operator and its extensions errors and elements which are included in the RxSwiftExt`library. They let you transform a sequence into a sequence of events containing original events and filter out either errors or values. I highly recommend using the RxSwiftExt library for many other convenience operators. You can find the relevant documentation here: https://github.com/RxSwiftCommunity/RxSwiftExt
The last thing that I would like to cover in this post are ways to avoid errors in the first place. There are two mechanisms that I want to mention — traits called Drivers and Relays.
Let’s start with Driver. It is a trait that is commonly used for binding data to the UI and has the following properties: it cannot error out, it observes changes to the main scheduler and shares its side effects. Using this, we can avoid terminating UI bindings and handle errors in a specified way. Let’s take a look at an example:
As you can see, in case of failure an error event won’t reach the rx.text property, but it will just pass nil to it. And as I mentioned, this all happens on the main thread.
Lastly, I’ll touch on Relays. The concept is pretty simple, they are wrappers for subjects that cannot emit error and completed events. We have a BehaviorSubject wrapper called BehaviorRelay, which also works as a replacement for a deprecated Variable type, and PublishRelay which wraps PublishSubject. If you are not familiar with these, check this out: http://reactivex.io/documentation/subject.html.
For example, if we want to create a BehaviorRelay and pass a value to it, it looks like this:
As you can see, you have to call accept instead of onNext as in BehaviorSubject
So, there are a few ways to handle errors when using RxSwift. Surprisingly, relying solely on error events from the framework might not be the best idea in all cases.
I hope you liked this post — stay tuned for more.
If you’d like to clarify something or have any questions, feel free to reach out!
Sephora's seductive appHow to leverage psychology to increase purchases
You’ve been cooped up for a while now.Graphic design trends in 2021
The bug has smashed ‘fast forward’ on many pre-existing trends, making the ecommerce space even more fast-paced and competitive than ever.8 ecommerce tips based on Shopify, IBM and Researchgate statistics.
The diaries of tech-savvy e-commerce developers 🐐Shopify — state of play — 2018/2019