Closed jeffbski closed 4 years ago
When I wrote wonka
I've debated the pros and cons of having error states in parallel to the stream's completion. There are multiple problems and reasons though why it's not included today.
map
or filter
, instead of blindly trusting those (So this is a very non-Reason/OCaml part of the answer)fetch
or other effects that will eventually output results. This can be modelled in userland by having type resultT('a) = Error(Js.Exn.t) | Ok('a)
or something similar in TS/Ocaml/etc. Hence this is concerned that at times errors are part of values of an asynchronous stream, rather than completion, so shortcircuiting isn't always necessary.End
signal carries an error, it would be the only "idiomatic" way for the library to carry errors, so the above case would then co-exist with shortcircuiting errors, even though shortcircuited errors are more common in the first, synchronous case.So all in all, I see this as handling unexpected errors. But the assumption is that unexpected exceptions are meant to be kept in check and not occur in the first place. For all other cases modeling errors as part of the stream's values seems acceptable in my opinion.
It's just heavily tied to the use-cases of Wonka, which aren't the same as other stream libraries. Hope that makes sense so far :)
I had expected that maybe you were dealing with them in a monadic way, maybe using Result or similar, especially with asynchronous results but it sounds like there's no special handling so every step would need to check the value on their own?
I'm not sure what you mean by unexpected errors, seems like we always hope that things will work as they should, but since we are often working with external systems and changing environments things sometimes fail.
It doesn't even look like errors from promises, observables, or callbags would even make it into the stream, so I guess they are just silently dropped?
I'm not sure I can really envision many use-cases where errors are not possible. Like you said fetch and any I/O can all have errors or fail to respond within an allowed time (generating a timeout error) so one should be able to handle those in an elegant way.
I guess if wonka isn't going to deal with errors you should probably explain that in the README very clearly. I'd also question whether it is valid to say that you interop with promises, observables, and callbags since it would only work with those that never generate errors.
Thanks for taking the time to explain this to me.
It doesn't even look like errors from promises or observables would even make it into the stream, so I guess they are just silently dropped?
Yes, currently there's no fromPromise
or fromObservable
helper that automatically wraps things in a result as that'd be placing some assumptions onto each environment, of which wonka supports several. The more expected case here is to wrap any effect that is expected to error using Wonka.make
, which can then be used to implement cancellation and wrap results in a monadic error wrapper.
I'm not sure I can really envision many use-cases where errors are not possible. Like you said fetch and any I/O can all have errors or fail to respond within an allowed time (generating a timeout error) so one should be able to handle those in an elegant way.
This library is definitely still very geared towards usage in our GraphQL client, so it depends on how we'd look at it. There we're already wrapping fetch
(or other transports) so that any result from the Wonka sources would contain errors: https://github.com/FormidableLabs/urql/blob/3329005c70b314be15a348bd7137cf446566c7dd/src/types.ts#L61-L73
I guess if wonka isn't going to deal with errors you should probably explain that in the README very clearly. I'd also question whether it is valid to say that you interop with promises, observables, and callbags since it would only work with those that never generate errors.
That's a good point :+1: I suppose the longterm solution is though to either provide wrappers that can deal with errors by using something like the resultT
type in the last comment or something else 🤔 I'll also have to investigate whether it wouldn't be more transparent to extend the End
event to include errors.
Thanks. yes, if one needs to wrap with a resultT or use Wonka.make then that would be great to see examples of that and maybe long term having something baked in.
Can someone please give a small example?
@ollydixon here is a little snippet of how I handle errors with wonka
and @mobily/ts-belt
(ts-belt is a library for FP in TS, highly inspired by ReScript’s Belt)
First off, we need to create the source creator that wraps a promise operation result in Result
, which has the following signature: type Result<A, B> = Ok<A> | Error<B>
import { R, Result } from '@mobily/ts-belt'
import { Source, make } from 'wonka'
class Failure {
error: Error;
constructor(error: Error) {
this.error = error;
}
get message() {
return this.error.message;
}
get stack() {
return this.error.stack;
}
}
const failwith = (error: Error | string) => {
if (typeof error === 'string') {
return new Failure(new Error(error));
}
return new Failure(error);
};
const fromPromiseR = <T>(promise: Promise<T>): Source<Result<NonNullable<T>, Failure>> => {
return make<Result<NonNullable<T>, Failure>>(observer => {
const { next, complete } = observer
const isCancelled = {
current: false,
}
promise
.then(result => {
if (!isCancelled.current) {
next(R.fromNullable(failwith('nullable'), result))
return complete()
}
})
.catch(err => {
if (!isCancelled.current) {
next(R.Error(failwith(err)))
return complete()
}
})
return () => {
isCancelled.current = true
}
})
}
and then, right after using fromPromiseR
, we can extract an Ok
value using ts-belt
helpers, in this specific example I'm gonna use R.getWithDefault
, which returns a value if a result is Ok
, otherwise, returns a default value.
import { R } from '@mobily/ts-belt'
import { map, subscribe } from 'wonka'
const fakePromise = <T>(status: 'resolve' | 'reject', value: T) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
status === 'resolve' ? resolve(value) : reject(new Error('oops!'));
}, 1000);
});
};
const defaultEmptyArray: number[] = [];
pipe(
// update 'resolve' to 'reject' to see how it handles errors
fromPromiseR(fakePromise('resolve', [1, 2, 3])),
tap((result) => console.log('EXAMPLE #1 – tap:', result)),
map(R.getWithDefault(defaultEmptyArray)),
subscribe((list) => {
console.log('EXAMPLE #1 – subscribe:', list);
})
);
here is the full example: https://stackblitz.com/edit/typescript-sqkail?devtoolsheight=33&file=index.ts
I've been using this approach in several projects and it works pretty well, hopefully, it will be a suggestion for you on how to tackle this problem :)
Looking through the readme, examples, and code, I don't see how to deal with errors whether explicit or whether handled with something like Result.
Obviously if one of the operators or sources returns an error, then it should short circuit the processing in the pipeline.
Since promises, observables, and callbags all can have errors, how are they dealt with?