-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The fail-hard nature of Observable errors lend itself ill-suited for certain applications #177
Comments
@isiahmeadows No arguments here 😄 I had opened a similar issue a while back and there was some discussion around it but I think folks lost interest in my pro "stay-open" ramblings. |
The trick is to gather all errors into a separate const error$ = new Subject();
Observable.of(1, 2, 3, 4, 5)
.map(n => {
if (n === 4) {
throw new Error('four!');
}
return n;
})
.catch((err, caught) => {
error$.next(err);
return caught;
})
.take(30)
.subscribe(x => console.log(x)); // 1, 2, 3, 1, 2, 3, ...
error$.subscribe(x => console.log(x)); // Error, Error, Error, ... |
I don't understand this - the error handling behavior of observables is similar to that of non-reactive code. If you want to handle an error in an (That is, assuming there's a I write code that recovers from errors with Rx all the time and it never felt weird. |
Also, a typical thrown error in plain JavaScript will abort subsequent synchronous code unless caught. Observables follow that behavior. |
Yes, observable sequences intentionally have the same semantics as simple iteration (i.e. |
Probably the best way to contrast this is:
From a type-theoretic perspective, this is the difference between Haskell's monads and arrows, where monads are a generalization of iteration, and arrows are a generalization of control flow. Similarly, mapping over an emitter is obviously iteration, but transforming the emitter could be seen as invoking a pseudo-function over each emitted value. Also, before I continue on, just thought I'd make this clear: I'm aware that these two are technically equivalent in theory, and that you could in fact model both fail-hard and fail-soft in terms of one another:
I'm aware that's an option, but it seems odd from a design standpoint that you should even need to link to a separate observable for error handling, rather than simply handling it inline.
This requires that you allocate an entire Observable pipeline for each request, when nearly every request results in at most one response. For concurrent requests, this would eliminate some of the advantages of short-circuiting, it'd complicate certain forms of middleware, and it's just flat out wasteful in certain spots.* So although it might work on the front end for UI error handling, this would not work for anything where high concurrency is the norm. (This is one of the key differences between monadic and arrow-based pipelines: memory requirements.) * Yes, I'm aware that Koa uses a similar process, but it has a few key differences that mitigate it:
I'm more commenting on an area where this generates boilerplate, and thus becomes pretty unintuitive for those who aren't as familiar with observables. I'm familiar with them, but one of the things that frequently drives me away from them is all the complexity from the vast number of operators required to sustain them. It's also why I suggested a while back creating a syntactic variant, something that would cut down on the number of operators you need and fit more in line with how observables are used today, similar to how async functions made promises a little easier to reason about. (Of course, I'd rather shoot back to the drawing board regarding observable-specific syntax than stick with what's in that proposal there, but that's beside the point.) |
I'm not sure I get this, monads never felt like a generalization of iteration when I was writing in Haskell. Arrows and Monads both felt like a form of flow control - especially variations of the continuation monad which explicitly feels like flow control - especially with syntactic assist and Having used both I also find the answer a bit weird to be fair.
Since an observable is like a function, chaining operators is like composing functions (rather than transforming streams) and subscribing to it is just like calling a function - I'm fine with invoking that composed function once per request. I've found that I needed this for more things than catching and recovering from errors - for example adding timeouts, logging instrumentation and various other things.
Well, I'm not sure how to respond to that because it certainly does work for production systems I've worked on that needed high concurrency as the norm. I agree it's likely not the fastest way to write code - but I'm not sure there's a performance concern of a chain-per-request. If that ever becomes a problem you can always use @staltz trick and gather errors that way.
This is optimization work, theoretically an observable should be a lot cheaper to create and use than a promise since it doesn't need to cache values or keep track of subscribers.
I acknowledge observables are a new concept for most people and they have a hard time learning it initially (like recursion, functional languages, matrix multiplication or closures) and I think a syntactic assist should be pretty nice. The problem is that this proposal is pretty old at this point and has been stuck at stage 1 for several years now and while I think
It's worth mentioning that IMO promises would look different if we added them with fully-specified async functions. |
Yeah...that was a bit inaccurate. It's really somewhere between "mapping" and "iterating". (Most key uses of observables aren't even from monadic joins - that's possible, but observables are more often traversed rather than bound together, where promises are more often joined.)
Only tangentially related, but a pipeline operator would solve most of the composability issues that currently exist with observables.
A promise can trash its list of subscribers as soon as it's resolved, and any decent implementation does. In practice, you could allocate arrays only as necessary and model its state as a pretty concise 3-variant sum type, which would result in very little memory footprint: enum PromiseState<T> {
Pending {
isHandled: bool;
fulfills: Vec<FnOnce(T) -> ()>;
rejects: Vec<FnOnce(T) -> ()>;
},
Fulfilled {
value: T;
},
Rejected {
isHandled: bool;
value: T;
},
} Conversely, an observable still has to keep track of exactly one subscriber object, and can't let it go until after it errors or completes. If that subscription includes I'll concede that pre-subscription, it's minimal, though, and it can specialize for just having one observer.
I'm also +1 on this, but I feel the core observable API really needs other features (e.g. a pipeline operator) before we start seriously considering most other potential issues with it. (The pipeline operator would make method chaining less of a requirement to be ergonomic, and I suspect it would dramatically change how many people use observables.)
I'd have to agree with this. Asynchrony is hard, but the async/await syntax encourages procedural code a little more than it should. |
Pipeline operator? You mean that thing that doesn't accept multiple arguments without an extra arrow function? :P |
@zenparsing Yes, although I don't feel it's ideal, and it's been discussed to death in that bug as well as this bug in the pipeline operator's repo. That ship appears to have sailed at this point, so I'm not really debating it any longer. 😛 |
I think it’s important that anything named “Observable” match the semantics of Iterable in the event of an error (principle of least surprise). As others have pointed out (and has been acknowledged), the catch operator can be used to handle common scenarios in which closing on error is not desired. I don’t find performance arguments compelling enough to complicate the API, particularly because I don’t think they have been demonstrated convincingly. Closing this issue for now. |
Hmmm I've just run into this now. In my situation there is a subject reporting HTTP results. On success it does a next with the data. On failure it does an error with the error. In no way does an error mean that future HTTP requests are unable to be completed... so it seems counter-intuitive that the subject completes on error. I propose a boolean setting for Subjects that prevents this (often inappropriate) behaviour... So you could set completeOnError = false (true by default), and then it will just spit errors and values out accordingly, without closing, until you actually do a complete. |
Supposedly, the correct way to handle this is to replace the active observable via Honestly, I'm coming of the impression observables are probably suboptimal for servers regardless of the outcome of this bug, because it's unclear what the response should look like (I can tell you a simple "return headers + stream" isn't always most efficient or even possible for HTTP1.1, and I'm still learning enough about HTTP2 to figure out how to address the shared connection situation). |
For what it's worth that's what Node.js does. This is the behavior with callbacks and has been forever - unhandled errors terminate the server. |
@benjamingr Catching exceptions isn't the same as crashing. I'm referring to the catching behavior, not the crashing behavior:
If you were to make errors non-fatal, all of that overhead would disappear, which was my point. I will reiterate this, though:
This was after trying for a while trying to see how a server could fit into the world of observables, and I'm beginning to believe that observables are exactly the wrong abstraction here, and that it needs to remain much more dynamic. If anything, I'm starting to believe the server side is relying too much on static material, to the point it's getting boilerplatey and unnecessarily bloated. It's much easier to do conditional routing when it's just an |
Currently, Observables close on error. This is well-documented, and most popular libraries implementing this spec have been doing this for a while. However, this limits its utility in certain areas where fault tolerance is either helpful or even required, and several reactive libraries similarly remain alive on error (just propagating the error down the stream until caught).
Observable<Request> => Observable<Response>
, but you can't let uncaught errors close it, for obvious reasons.So, my thought is this:
error
andcomplete
, but it needs to be done atomically.So given this distinction, I feel it might be better if there was a failable variant introduced for both sending and receiving. They could still be part of the observable umbrella, but it'd open up a whole host of new possibilities.
Here's a list of some libraries I know of, and how they handle errors:
The text was updated successfully, but these errors were encountered: