0no-co / wonka

🎩 A tiny but capable push & pull stream library for TypeScript and Flow
MIT License
709 stars 29 forks source link

How do you deal with errors? #39

Closed jeffbski closed 4 years ago

jeffbski commented 4 years ago

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?

kitten commented 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.

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 :)

jeffbski commented 4 years ago

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.

kitten commented 4 years ago

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.

jeffbski commented 4 years ago

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.

ollyde commented 2 years ago

Can someone please give a small example?

mobily commented 2 years ago

@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 :)