SolvedRxSwift Revisit typed errors?

We discussed typed errors here on github a while back. Maybe you've discussed on slack as well. I thought maybe it is time to revisit this question.

Unambiguous API
The user of your API doesn't need to make assumptions. She/he will know what errors your API produces. So will your unit tests.

Let the compiler do the job
Type safe languages have the benefit of letting the compiler do work that you normally have to unit test. Does PushHandler return any other error than PushHandlerError? No need to unit test mundane stuff like that if the compiler can tell you right away.

Sequences that can't fail
Some sequences can't fail and that is valuable information when dealing with them as it makes code less engineered and less complex.

~~

RAC has a great writeup on how working with typed errors is like .

Would it be possible to change the API in such way that it is possible to opt-in to typed errors?

I love typed errors, but hey, maybe that's just me :)

54 Answers

✔️Accepted Answer

It looks like Swift 5 introduces Result<Value, Error: Swift.Error>.

This thing compiles:

struct Observable<Value, Completed, Error> { }

but it would be ideal if

struct Observable<Value, Completed = Never, Error = Never> { }

would also compile.

As I see it we could introduce something like

struct ObservableXXX<Value, Completed, Error> { }

and make

typealias Observable<Value> = ObservableXXX<Value, (), Error>

I think that if we did that it would be win/win situation because we would maintain backwards compatibility and provide more type safety options.

Once Swift adds default generic arguments, we could just make:

struct Observable<Value, Error = Never, Completed = Never> { }

1rst stage: First provide additional features and type safety together with more dynamic API.
2nd stage: Make a breaking change by effectively changing from:

struct Observable<Value, Completed = (), Error = Error> { }

to

typealias DynamicObservable<Value> = Observable<Value, Completed, Error>
struct Observable<Value, Completed = Never, Error = Never> { }

I think we could implement first stage for RxSwift 5.0. When should we make the second stage is hard to tell because there is a lot of legacy and educational code out there. It would be ideal if Swift 5.0 introduced default generic arguments and if we just made it all at once, but it seems like this is not the case.

For the second stage one could make a global rename of Observable -> DynamicObservable, so it's not a horrible breaking change.

Anyway, these are my thoughts.

Other Answers:

Hi, guys!

I personally agains typed errors! :)

  • they do not provide any value (in my opinion)
  • introduce unnecessary mess (flatMapError, mapError, attemptMap , NoError and family)
  • swift does not support typed throws , and even if it would I would not like RxSwift to have typed errors (look at RxJava)
  • it gonna break cross language similarity

Also I feel like it violates duality math behind Rx

@hfossli that's a great point in general but I also feel like it's a "developer concern" and not necessarily a RxSwift concern. The "official" ReactiveX standard is failing on sequence error.

I wouldn't be opposed to a type of Observable that can't fail / provides typed errors but I'm not sure it's exactly in the scope of this library ...

Also, some people rely on ReactiveX-based implementations to work the same across platforms so having this "discrete" implementation that is only relevant to RxSwift could be highly confusing and needs to be well thought out IMO.

In 99% of the cases these issues are resolved with using a Result type, which is just a "standard" part of RAC, really. RxSwift gives you the freedom of choosing how you want to model your results and doesn't "bind" (pun not intended) you into a specific format (for better or worse)

Just me $0.02

Also in my opinion Observable based API are perfect example of Open Closed Principle, looking on API like:

func getSomething() -> Observable<Something>

The data may come from cache, network, streaming connection or both. It allows to model you app without worrying that changes of platform will break something. Adding typed error will most likely break the client if you change type of the error or add another case to your Error enum

Anyway I think I understand a current state of things now. I think it is ok to have two FRP frameworks, one more strict (ReactiveSwift) another one is less strict (RxSwift) ...

I don't think this sentence characterizes the spirit of this project correctly and there isn't a parallel between errors from RxSwift and ReactiveSwift.

Both RxSwift and ReactiveSwift have something named error in their events, but they aren't conceptually interchangeable.

You would use error in RxSwift as a short switch:

  • Something unexpected happened
  • I don't know how to handle this, just send it to server and log it, I have no idea how this is happening (and yes, delete the logs because of GDPR after you've figured out what the hell is happening)
  • But I also don't want to kill the entire app because of it.

It is not recommended to encode any business logic in errors, catch them and then parse the error information. This is anti-pattern IMHO.

I'm usually using resultish values as sequence elements because you want the compiler to notify you if you've forgotten to handle some case.

There are some type safe properties that this project offers that some other projects don't:

  • compile time guarantee that the sequence generates single, 0-1 or none elements (Single, Maybe, Completable)
  • compile time guarantee that you won't have deadlocks and that you can safely reason about multi-operator compositions without worrying about data races, sharing strategy guarantees (Driver, Signal and other traits from RxCocoa)
  • there are also guarantees that certain sequences won't error out (Driver, Signal and other traits from RxCocoa), but yes, there are no general guarantees regarding erroring out.

ReactiveSwift could also advocate that their Signal, Properties, etc... are the latest and coolest things ever.

... and let users choose what they want. Cheers. :)

Sure 😆 🥂 🥂 🥂

There is just one tiny detail nobody is mentioning, so I'm going to mention it anyway. Maybe somebody will find this information useful.

There are actually 3 events in Observable (or 4 in RXS/SignalProducer) and for some reason everybody is forgetting about Completable event (or Completable/Interrupted in RXS case).

I can't be entirely sure why people are advocating for parameterized errors, but I'm assuming that they are probably advocating this because they want to be sure that they've handled all of their errors, and their user interface won't die because they've forgot to handle an error (that's one of the reasons why we have Driver/Signal in RxCocoa).

BUT

Even if we parameterized Error type, that still wouldn't be enough to guarantee that your user interface won't die. Your sequence can Complete and you also need a guard against that.

This is not just theoretical concern, consider this code:

let iCanSometimesErrorOut: Observable<Int, Error>;
let iMSafeToBindToUI: Observable<Int, NoError> = iCanSometimesErrorOut
     .catchError(x => Observable.empty())

Hint: If you've found this comment, more useful approach is maybe retry family of operators but be careful with them, because you could accidentally overwhelm your service and create a DDOS attack on your service if you don't properly design a retry strategy, so try to implement some binary back-off retry strategy to prevent these kinds of issues.

This is perfectly safe to bind to user interface, but it doesn't prove compile time that your code is correct just because it has NoError. You UI will still die even if no error is thrown. Complete event is also a silent killer.

I'm not trying to say that reminding the user that somebody hasn't handled the error properly isn't a nice handy thing (I find this really useful with Driver/Signal from RxCocoa), but that it doesn't provide you with guarantees that one would assume from the code itself.

The case I've presented isn't theoretical. I've been watching people day after day people writing code like this (usually with Driver, but nothing in the interface would prevent people from doing the same thing with Observable if we added typed errors). If you are thinking the problem was people, I'm not that convinced, these were very capable people IMHO that I respect, but they just needed time like everyone else.

One could think, ok, let's add typed complete event.

let iCanSometimesErrorOutAndComplete: Observable<Int, Error, ()>;
let iMSafeToBindToUI: Observable<Int, NoError, NoComplete> = iCanSometimesErrorOut
     .catchError(x => Observable.never()) // <--- just change to never

This again won't work. One can just replace empty with never and you are back to square one. Your program type checked, your sequence will never complete, but your user interface is dead again because it won't ever receive any elements.

If errors regarding .catchError(x => Observable.empty()) are common, it is reasonable to assume that errors regarding .catchError(x => Observable.never()) would be common somewhere approx same order of magnitude.

But yes, we don't have NoError type in Observable, sorry ¯\_(ツ)_/¯

More Issues: