neverthrow: Why can't I return different error types in a "andThen" chain?

Hi,

Thank you for publishing this!

I wanted to get started right away but ran into a seemingly trivial problem I am too much of a noob to solve. Any help would be appreciated!

The compiler complains when I want to compose multiple results with the .andThen() method

type E1 = 'invalid'
type E2 = 'also invalid'
type E3 = 'invalid again'
type Errors = E1 | E2 | E3

const f1 = (sth: unknown) => Result<string, E1>
const f2 = (input: string) => Result<string, E2>
const f3 = (thing: string) => Result<string, E3>

const result: Result<string, Errors> = f1('test')
                         .andThen(f2)
                         .andThen(f3)

The compiler complains that that the result of f1 is not assignable to the result of f2, specifically Result<string, E1> is not assignable to Result<string, E2>.

I thought that the mismatch of results and function parameters wouldn’t hurt, because .andThen() doesn’t even call the next function if the result is of type Err. Any idea how I could help with the type inference?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 20 (16 by maintainers)

Most upvoted comments

Alright folks … After 8 months of thinking through this, I think I am finally persuaded to go down the route of joining types.

I’m planning on doing some work soonish on this. Maybe I’ll get around to it this week.

I realized that because Rust and Haskell and basically all the other well-known languages that have a Result don’t have subtyping. So it’s literally impossible to do T | A … it’s just not part of those languages. But in typescript, this is allowed!

Hi there, I’ve been following this discussion for quite a while now from the sidelines and I do understand @supermacro 's reservations (to some extend)

Going back to one of your past comments:

Yes, I could use a single error type E, add a type string literal to it like in my example above, and discriminate using the type property.

I still stand by this approach. Which is what I use when merging various error types in a long chain or results

This is definetely an idioamtic approach in most circumstances, and you are right that andThen or bind or however the equivalent is called in other languages doesn’t allow a different error type to be returned (AFAIK).

BUT this approach comes with a cost (and is also sometimes criticized in the respective communities): composability on the macro level can only be achieved by sacrifizing type expressiveness at the mirco level. @supermacro has demonstrated this in one of his previous examples pretty well. Compare: (modified from the OP)

type E1 = number
type E2 = string
interface E3 {
  errorCode: number
  message: string
}

type MyUnion = E1 | E2 | E3

type F1 = (sth: unknown) => Result<string, E1>;
type F2 = (sth2: string) => Result<string, E2>;
type F3 = (sth3: string) => Result<string, E3>;

//...Implementation

const result: Result<string, MyUnion> = f1('maybe').andThen(f2).andThen(f3)

with the proposed solution:

type F1 = (sth: unknown) => Result<string, MyUnion>;
type F2 = (sth2: string) => Result<string, MyUnion>;
type F3 = (sth3: string) => Result<string, MyUnion>;

//...Implementation

const result: Result<string, MyUnion> = f1('maybe').andThen(f2).andThen(f3)

I left out the dicrimininator but I think my argument still stands: In order to compose arbitrary functions returning Result types, one has to alter or overload their signatures. Thus reducing the expressiveness of the signature.

Additionally it is not possible to centrally handle Result types at the moment, because the error types of a given pipeline never line up.

I was drawn to this GREAT library, because it allows me to be expressive with error paths. At the same time, the possibility to easily compose different functions without them having to know the context in which they are composed would be a really BIG WIN. Especially, since the propsed changes by @paduc are minimal and non-breaking, I would be in favour of changing the interface.

I hope you don’t mind me jumping in this discussion.

I see how having a union Error type can solve the issue but I’m not sure it’s the only solution.

We could also have andThen return an error of type of the union of possible errors in the chain.

Example:

// Let's say we have the following functions
const validateUserObject: (user: User) => Result<User, InvalidUsernameError>
const insertUserToDb: (user: User) => Result<User, DbError | UsernameTakenError>

const res = validateUserObject(user).andThen(insertUserToDb)
// We could have
typeof res = Result<User, InvalidUsernameError | DbError | UsernameTakenError>

// This way, we can handle the specific errors
if(res.isErr()){
   if(res.error instanceof InvalidUsernameError || res.error instanceof UsernameTakenError){
      res.send({error: "Please choose another username"})
   }
   else {
      // We know it's a DbError
      res.send({error: "Service unavailable, please try again."});
   }
}

The only change necessary would be to change the signature of andThen to

// In Ok<T, E> or Err<T,E>
andThen<U, F>(_f: (t: T) => Result<U, F>): Result<U, E | F>

The implementation itself doesn’t need to change and the change would be non-breaking because E | E === E, meaning that if you have code that chained multiple Result with the same error type, the union would be of that error type. It wouldn’t interfere with the short-circuiting of the function in Err.

I see this capability as interesting when working with an external module that returns a Result<T, ModuleSpecificError>. It’s error type is not necessarily compatible with my local error type (maybe the error is in a message property while my local errors use an err property).

It’s also a nice way of having the compiler tell me exactly which types of errors can occur in my chain.

Alrighty, I have published a new version behind a beta tag for now.

> npm install neverthrow@beta

Release notes:

https://github.com/supermacro/neverthrow/releases/tag/v4.1.0-beta.0


I am closing this issue, but feel free to open a new issue regarding this topic!

Hurray ! Tell me if I can be of any help !

Hi @supermacro !

I would definitely welcome a cleaner alternative to my “hack” (it’s a one-time thing though).

A cli-tool seems a fair bit of work. I have seen npm packages that provide “alternate” modes for their libs and maybe neverthrow could do the same.

Something like:

import { ResultAsync } from 'neverthrow/unionErrors`

I’m still hoping this could be the default behaviour btw 😉