Error Handling Approaches

Yesterday’s post aside, we’ve spent the last several days discussing RxSwift and Combine:

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 of Error 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 specific Error type every time you create a stream, much less creating semantic errors for every stream.
  • Specifying specific Errors means you always know the exact kind of Error 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 any Error could 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.