-
Notifications
You must be signed in to change notification settings - Fork 97
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
Error handling and fromPromise() #265
Comments
Unfortunately, that's how promises work, and there's not really a whole lot we can do about that. I've run into the same problem, and I've mostly worked around it by not interoperating Promises with Observables. For example, I wrote a wrapper around XMLHttpRequest for a project (which I'll eventually extract into a separate library). But yeah, that's just how they work. I don't know if there's a good way to get around that. |
In any case you shouldn't lose a stack trace. This is clearly a buggy behavior. import K from "kefir"
K.fromPromise(Promise.resolve("foo")).map(() => {
return x.y
}).observe(
(x) => console.log("x:", x),
(e) => console.log("e:", e),
) kefir.js (v1)function fromPromise(promise) {
...
var _promise = promise.then(onValue, onError);
...
} produces
kefir.js (v2)function fromPromise(promise) {
...
var _promise = promise.then(onValue).catch(onError);
...
} produces
kefir.js (v3)function fromPromise(promise) {
...
var onValue = function (x) {
setImmediate(() => {
emitter.emit(x); // this time .emit is not trapped inside promise .then and
emitter.end(); // behaves like in normal promise-less streams (see above)
})
};
...
} I think v3 is what we need because we get a normal error reporting:
and syntax/operational errors are now separated. Syntax errors can't be handled (they crash the process in Node as they should). |
The problem is that we have to execute some code inside kefir/src/interop/from-promise.js Lines 9 to 17 in db4ea0d
A possible solution could be to minimize code that we execute in |
@ivan-kleshnin The only issue w/ v3 is you still end up without the stack trace, for better or worse.
If we use If we're considering this a "bug", is there any way / desire to interop with a "Promiseish" library (e.g. Fluture or fun-task) that doesn't do this? I don't know how much that helps, but may be a useful workaround if you're trying to get away from this behavior. |
Then you mix operational errors with syntax errors just like RxJS does. I don't think it's a good behavior. For Kefir, it won't be acceptable that
in one case throws, and in another pipes an error to error handler in
There is a normal stack trace. I showed the first line of it.
To be honest, I think try-catch is an antipattern. It conflates the errors of entirely different natures. In case of JS & AJAX it's: syntax errors + dynamic errors + connection errors + 404 NotFound + 500 ServerError... An absurd. Now promises make an emulation of The best decision IMO is to make |
Not sure about current support in browser, but we'll probably have to use the hack with iframes then, which always seemed silly to me. But I won't object too much, Most and Rx probably use that hack too so... If we use
Yeah, I also don't know how this would help. It would be nice to have integration with these libraries, but we need an integration with Promises anyway.
The way promises work don't leave us much of a choice 😉 |
At this moment, Kefir does agressive error catching. I'd like this to be changed if possible. Interoperability with promises does not necessary mean we have to accept their patterns of thinking. |
Adding a |
@agentme promises are async already. I think you should show the code you mean, so we can test it with both approaches. |
Yeah, |
@ivan-kleshnin Promises are async, but they're part of the microtask queue, so they get executed as part of the current task which the browser doesn't render during. Compare these two silly snippets of code in the console on a simple webpage: function f() { document.body.style.backgroundColor = 'green'; setTimeout(()=>{ document.body.style.backgroundColor = 'white'; setTimeout(f, 100); }, 0); }
f(); function f() { document.body.style.backgroundColor = 'green'; Promise.resolve().then(()=>{ document.body.style.backgroundColor = 'white'; setTimeout(f, 100); }); }
f(); In modern browsers that correctly implement promises and microtask queues (this includes Firefox and Chrome at least, no idea about IE), then in the second one, you'll never see the page's background color rendered green. (Browsers are free to render the background color green briefly in the first example. Chrome tends to show it every single time, but Firefox skips it occasionally for me.) This is a silly example, but I find it common to do things equivalent to this. Consider a @rpominov One problem is that setImmediate is not implemented on Chrome (and isn't standardized so easily might not be in other or future browsers). It could be used if available. I wonder if the inconsistency in how errors get handled might surprise people, but I don't feel strongly about that since Kefir doesn't have any real existing story about handling exceptions. |
That bothers me too. We could use polyfills (like RxJs does for example), although I don't like the idea of having these hacks in codebase to be honest. P.S. |
My mistake, you're correct. I thought you'd lose part of the stack trace because you'd lose it's connection to the promise, but I realize now that's a devtools feature, not a promise error stacktrace feature, and actually would include the
It would help insofar as it gives users another non-error-swallowing option. The issue is there aren't really libraries built on them, e.g. I don't know of an AJAX lib built on Fluture, and a quick google search didn't turn up anything obvious, so in all likelihood, if userland needs a Fluture-based AJAX lib, they need to write it themselves, at which point, they might as well just write the lib in Kefir. So maybe it doesn't really help 😕
We're only "doing" that error catching because we're relying on Promise's default behavior, but I think we all agree with you that Promises doing this is... not great. @rpominov wrote a whole thing about it here. So the debate isn't if it's good or not so much as whether and what we can do about it.
Wait, it isn't? I tested this in my browser console w/o issue: If it's not, then yeah... we've got a bit of a problem, because as @agentme points out, the task would no longer be added to the same queue and the behavior changes. |
@mAAdhaTTah Did you check for Actually Firefox doesn't implement it either. ... I checked FF while on Github lol. |
I did not, but I guess whatever site I was on had it. Although the console said That still leaves us without a good solution here without breaking / changing the semantics, unless we want to require a |
I tested in NodeJS and stack is fine there as well. Not sure what you mean by "devtools". Anyway, guys I totally understand it's easier to say than done, so I rely on your decision here. @mAAdhaTTah thanks for the link to fun-task. I didn't pay attention to this repo (yet) so I'll take my time to check it. |
So to recap, here are our options:
Did I miss anything in the list? Anyone got any other potential options here? |
I have a potential option: remove Promise interop altogether and push that out into a separate library where we can document the differences in behavior, or even multiple packages that handle it different ways, depending on consumer preferences. |
Seems legit to me.
https://github.com/YuzuJS/setImmediate/blob/master/setImmediate.js This polyfill is not tiny, but not super big either. The option to remove Promise support from the core is also interesting. It seems there's not many bindings to them in Kefir. |
Yep. Maybe we should deprecate We could even leave |
I dig this. If we're going to break one out to a separate repo, I'd prefer doing both |
Node.js's default unhandled promise rejection handler only shows the error message without its stack trace. You can make it show the whole stack trace by adding an unhandled promise rejection handler, which is probably a good idea if you're using promises at all regardless of Kefir: process.on('unhandledRejection', error => {
console.error('unhandled promise rejection', error);
});
Kefir.fromPromise(Promise.resolve()).onValue(() => {throw new Error('test error');}); There is a stalled bug report about node.js's default unhandled promise rejection handler being absolutely useless. Chrome's console always allows seeing the full stack of errors from unhandled promise rejections. Most of Kefir doesn't try to handle exceptions at all as it is, and promises are increasingly core to javascript with the popularity of async functions (using |
@agentme the problem we discuss is not limited to stack trace. It's about inconsistency with error handling current As I showed above, we can get our stack back by replacing
Exactly. And current |
The issue with the current Promise interop is that this: Kefir.fromPromise(Promise.resolve()).onValue(() => {throw new Error('test error');}); even throws an
I personally never use Kefir in that way; for me, if I'm using Observables, it's Observables all the way down. I use
Is this to suggest that you'd want to leave the current behavior in the library and also add the second package suggested above? |
After reading this https://github.com/rpominov/fun-task/blob/master/docs/exceptions.md and reconsidering my programming experience, I'd like to add some additional info and code examples. I come to the conclusion that error handling in streaming library is only useful in handling "Expected failures". "Unexpected failures", as noted, are impossible to handle except logging (remote HTTP "alerts" also fall into this category). And for logging, a singleton global event listener should be more than enough. And we have one in both Browser and Node. Unfortunately for us, the authors of Promise A+ spec have chosen a different path and How it should look like, being done correct? In the same way in both streams and promises – operational errors (expected failures) are stopped by As I mentioned task.run({
success(x) {
// handle success
},
failure(x) {
// handle expected failure <-- Yes! This is great!
},
catch(e) {
// handle a bug <-- I woudn't support this feature. Bugs shouldn't be handled locally, i.m.o.
},
})
Yes! But, actually, it may suddenly become a good one(!) if we:
Then we can make a coresspondance between The following code demonstrates a fetch of blog posts (all at once, no pagination). Fetching is done in two steps: // A is Axios, K is Kefir, R is Ramda
// intents: Object (Stream *)
let intents = {
// HTTP
fetch$: sources.state$.sampledBy(K
.fromPromise(A.get("/api/posts/~/id")) // promise error -> stream error
.map(resp => R.pluck("id", resp.data.models)),
(state, requiredIds) => {
let presentIds = R.keys(R.view(baseLens, state))
let missingIds = R.difference(requiredIds, presentIds)
return missingIds
})
.filter(R.length)
.flatMapConcat(ids => K
.fromPromise(A.get(`/api/posts/${R.join(",", ids)}`)) // promise error -> stream error
.map(resp => resp.data.models)
),
}
...
// action$ :: Stream (State -> State)
let action$ = K.merge([
// Every such "channel" may encounter MANY errors of a SINGLE type (N chans <= N types)
intents.fetch$.map(posts => {
return function afterFetch(state) {
return R.over(baseLens, R.mergeFlipped(posts), state)
}
}).flatMapErrors(err => {
console.warn(`Request to "${err.response.config.url}" failed with message "${err.response.status} ${err.response.statusText}"`)
return K.never() // can modify state to add alert box here!
}),
]) Now if we make a code error like: fetch$: sources.state$.sampledBy(K
.fromPromise(A.get("/api/posts/~/id")) // promise error -> stream error
.map(() => x.y.z), // !!! it crashes and don't end up being mixed with operational errors. It works this way only with .flatMapErrors(err => {
console.warn(`Request to "${err.response.config.url}" failed with message "${err.response.status} ${err.response.statusText}"`)
return K.never() // can modify state to add alert box here!
}), we can't tell what is what, and things are getting out of control... Being tied to |
I know I never got around to adding this package, but a |
Let's compare error handling in RxJS and Kefir:
RxJS. Example 1
Kefir. Example 1
As we see, RxJS mixes operational and syntax errors which I consider a very bad idea.
Both end up being handled in the same function. Kefir separates two error types. Only operational errors will be caught and handled by our custom
(e) => ...
lambda.At least, both libraries show us the correct file, line and column.
Now let's take a look at Promise based streams:
RxJS. Example 2
The same behavior as in Example 1.
Kefir. Example 2
Now this one is broken. Kefir looses an error stack trace and the cause of our syntax error is lost.
For some reason, the promise "sucked" the error in. It's neither handled by error handler like in RxJS, nor escaped to the process like previously 😞 As a consequence, any promise-based stream in Kefir is a hell to debug right now.
I expected to get a process crash from sync errors in both Kefir examples.
The text was updated successfully, but these errors were encountered: