Skip to content
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 in flyd (Proposal) #171

Closed
nordfjord opened this issue Mar 11, 2018 · 6 comments
Closed

Error handling in flyd (Proposal) #171

nordfjord opened this issue Mar 11, 2018 · 6 comments

Comments

@nordfjord
Copy link
Collaborator

nordfjord commented Mar 11, 2018

I want to propose we implement the bifunctor spec to facilitate error handling.

If we look at the synchronisation quadrant

Flyd belongs in the asynchronous multi-value category (observable).

Looking at the Single value asynchronous landscape in functional fantasy-land compliant javascript one of the outstanding libraries is Fluture

They implement Bifunctor, Monad and ChainRec.

I would like to propose that flyd does the same.

This will allow users of flyd to operate with the same interface on single-value and multi-value async data.

There are two methods of note WRT error handling:

bimap :: (Bifunctor f) => f a b ~> (a -> c) -> (b -> d) -> f c d
fold :: (Bifunctor f) => f a b ~> (a -> c) -> (b -> c) -> f c d

A contrived example using Fluture

const example = Future.encaseP(fetch)('/example.json')
  .chain(res => future.encaseP(()=> res.json())())
  .bimap(tap(success => console.log(success)), tap(error => console.error(error)))
  .fold(Either.Right, Either.Left)

equivalent code in flyd if we go this route:

const example = flyd.fromPromise(fetch('./example.json'))
  .chain(res => flyd.fromPromise(res.json()))
  .bimap(tap(success => console.log(success)), tap(error => console.error(error)))
  .fold(Either.Right, Either.Left)
@StreetStrider
Copy link
Contributor

@nordfjord where does Either come from in this example? Is it implemented in Fluture or it «any user supplied Either»?

@nordfjord
Copy link
Collaborator Author

Any Either will do,

e.g.
data.either
sanctuary Either

Or anything really. Pick your poison :)

@nordfjord
Copy link
Collaborator Author

I think the most important notion in this proposal is the fact that it allows seamless interop between fluture and flyd.

This means that if at any point you realise that your future code really needs to handle multiple values the recourse is as simple as replacing Future with stream, and boom 💥 your code now handles multiple values.

I think the error handling capabilities it provides are a nice bonus though.

@nordfjord
Copy link
Collaborator Author

I've tested this out a bit using my own codebase, but rather than making flyd a bifunctor I've just been hosting an either inside the stream

const promiseToEitherStream = <T>(promise: Promise<T>) => {
  const s = stream<Either<Error, T>>();
  promise
    .then(val => s(Right(val)))
    .catch(err => s(Left(err))
    .finally(()=> s.end(true));
  return s;
}

This has actually been quite sufficient for my error handling use cases.

I'm closing this issue because of that.

@StreetStrider
Copy link
Contributor

Got some thoughts today. Barely ideas, not a strong concept.

  1. First of all, when I'm in promise land, any sync throw inside promise flow accurately catched as rejections. This gives me some sort of safety and guarantees that all errors are catched, no matter caused by JS builtin exception system or promise rejections. This is cool, for instance third party libraries can throw, and doing that inside stream will break it (or developer's own errors).
  2. We got special end stream, attached to any stream. Maybe it is possible to attach special error stream as well? It should catch exceptions during combine. To prevent errors from silently missing we can rethrow error if no handling on error stream.
  3. I don't know should stream end when error occur, or should be any recover mechanism.
  4. Combining streams should also merge their error parts.

@nordfjord
Copy link
Collaborator Author

Hey @StreetStrider

I think we can agree on a few things.

  1. Flyd should not swallow user errors
  2. Flyd should not break on user errors

The question is then how do we solve both.

My thesis in this issue was that we could make Flyd a bifunctor to better model the same flows we see in libraries like Fluture.

However, I also really like composing smaller things together to make larger things. I think if we can get away with just supporting those two use cases (not swallowing errors, and not breaking on errors) we allow the end user to pick their error handling strategy. An example would be using Either.

e.g. imagine:

const s = stream();
const errorable = s.map((val)=> {
  if (Math.random() >= .5) throw new Error('hahahhaa');
  return val * 2;
})

s(1)(2)(3)(4)(5);

I imagine that it's much superior for the user to be able to do something like:

const s = stream();
const errorable = s.map(Either.try((val)=> {
  if (Math.random() >= .5) throw new Error('hahahhaa');
  return val * 2;
}))
.map(console.log);

s(1)(2)(3)(4)(5);

A solution like the above

  1. Allows us to keep flyd minimal
  2. Allows users to decide on how to handle errors.

But that is predicated on us not violating the two use cases.

You could also implement something like:

const safeMap = curry((fn, s)=> combine((s, self)=> {
  const value = Either.try(fn)(s());
  value.map(self).leftMap(self.end);
}, [s]));

const s = stream();
const errorable = s.pipe(safeMap((val)=> {
  if (Math.random() >= .5) throw new Error('hahaha');
  return val * 2;
}));
s(1)(2)(3)(4)(5);

if you want your stream to end on errors.

With the example above I think we might want to allow values other than true in end streams.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants