Yesterday’s post aside, we’ve spent the last several days discussing RxSwift and Combine:
- How do we get to Observables?
- What is RxSwift anyway?
- Where are UIKit bindings in Combine?
- What’s new in Seed 2? Can KVO save us?
In Monday’s post, I said the following:
In order to discuss Combine, one has to discuss the main differences between it and RxSwift. To my eyes: there are three.
- Affordances for non-reactive classes
- Error handling
- Backpressure
We covered the first — bridging to non-reactive classes — in Monday’s and Tuesday’s posts. Today, let’s discuss error handling.
Going Back to the Beginning
If you recall, in our first post, we built up our own Observer
type by hand. This is where we landed:
protocol Observer {
    func onComplete()
    func onError(Error)
    func onNext(Element)
}
Note, in particular, the way errors are handled:
func onError(Error)
Herein lies the dramatic difference between RxSwift and Combine.
What even is an error, anyway?
In Swift, all errors can be eventually traced back to a single
protocol Error. This protocol is basically just a marker;
it doesn’t carry with it any particular functionality. This is
wonderful, because it makes it exceptionally easy to quickly
create a class, struct, or even an enum that is a valid,
throwable error.
When it comes to Observables/Publishers, there are two basic
approaches that API designers can choose between:
- Assume every stream can end in an Error, and not get specific about what kind ofErrorit is.
- Specify up front precisely what kind of Errorcan be emitted
There are benefits to each approach:
- Assuming any Errormeans you don’t have to be bothered with specifying a specificErrortype every time you create a stream, much less creating semantic errors for every stream.
- Specifying specific Errors means you always know the exact kind ofErrorthat could end a stream. This leads to better local reasoning, and the errors are more semantically meaningful.
Naturally, there are also drawbacks:
- Assuming any Errormeans literally anyErrorcould end any stream. You never really know what could pop out at the end of a stream until it happens.
- Specifying specific Errors means you must be explicit, always, about what could end every stream. This is a not-inconsequential amount of overhead and bookkeeping.
Error Handling in RxSwift
RxSwift takes the first approach.
In RxSwift, every stream can error with any kind of Error.
Naturally, the advantage of this is a dramatically reduced
amount of bookkeeping. One doesn’t need to worry about specifying
what error type may be emitted, because the answer is assumed:
any Error can be emitted.
However, that also makes it a little harder to understand what
can go wrong, or perhaps, how it can go wrong. Literally
every error in Swift is also an Error. Thus, it is —
from a type system perspective — possible for any
Error to be emitted from any stream.
Error Handling in Combine
It’s easy to guess what happens on the other side of the fence.
In Combine, every Producer (/Observable) must specify
the exact Error type up front.
This leads to a bit more bookkeeping; any time you create a
Producer you must also specify what type of Error that
Producer could emit. The advantage here is that you know
exactly what kind of Error may be emitted. If not a precise
type, at worst, a type hierarchy where the base is known.
That improves both local reasoning, as well as semantic meaning.
Furthermore, one can cheat a couple of different ways. There
is nothing stopping you from specifying the Error type as…
well… Error. That puts us basically in the world of
RxSwift: a stream that can emit any Error.
Additionally, one can really really cheat by using a
special type in Swift: Never.
Never is a special type that, by design, can never be
instantiated. (Behind the scenes it is an enumeration
that has no cases). If the error type in a Producer is
Never, guess how often that Producer can error? Not once.
Not even a little bit.
Which is better?
This is a case wherein the delta is simply that: a difference. Sitting here today, I can’t say whether one is better or worse than the other. The lazy developer in me isn’t overjoyed by the thought of all the additional housekeeping in Combine. However, the purist in me admires the clarity of specifying specific errors.
If I were to guess, I’d assume that I’ll start by complaining and moaning about the additional bookkeeping, and then eventually come around to the clarity of Combine’s approach.
Next Steps
In the next — and possibly last — post, I’ll explore the final of the three major differences I’ve spotted between Combine and RxSwift: backpressure.