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,
throw
able error.
When it comes to Observable
s/Publisher
s, 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 ofError
it is. - Specify up front precisely what kind of
Error
can be emitted
There are benefits to each approach:
- Assuming any
Error
means you don’t have to be bothered with specifying a specificError
type every time you create a stream, much less creating semantic errors for every stream. - Specifying specific
Error
s means you always know the exact kind ofError
that could end a stream. This leads to better local reasoning, and the errors are more semantically meaningful.
Naturally, there are also drawbacks:
- Assuming any
Error
means literally anyError
could end any stream. You never really know what could pop out at the end of a stream until it happens. - Specifying specific
Error
s 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 enum
eration
that has no case
s). 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.