Tom MacWright

[email protected]

TIL with neverthrow

I've been doing a lot of refactoring into neverthrow recently and hit this surprising case today:

import { ok, err, Result } from "neverthrow";

// Have one function that returns a Result<string, Error>
// value. In this case, the 'ok' type shouldn't matter 
// because we never refer to it and only use its
// error type.
function getPerson(): Result<string, Error> {
    return err(new Error('Oh no!'))
}

// And then a method that returns a Result<number, Error>
// type, which we will only use the number part from.
function getLength(name: string): Result<number, Error> {
    return ok(name.length)
}

// Combine them, and get a surprising result!
function combined() {
    const p = getPerson();
    if (p.isErr()) return p;
    return getLength(p.value);
}

// Even though we only took the error side from
// getPerson, we still have its return type (string)
// lurking in the return type from combined!
const r = combined().map(val => {
     val
  // ^? (string | number)
})

See this code in the TS playground! Basically this is somewhat surprising because I might guess that the .error part of a Result is narrowed to a Result<never, Error>, but I guess it isn't!

The solution was to reconstruct an error type with no ok side:

if (p.isErr()) return err(p.err);

I know, this code isn't as functional-programming aesthetic as it could be, but like most things that have users, the Val Town codebase is pragmatic and non-pure.

Overall my feelings on neverthrow so far:

  • It's pretty practical and has yielded a big improvement for us.
  • The learning curve is a lot less steep than Effect, and the documentation is a lot better, but it's still another thing to learn, and it's easy to in ways that eat a lot of the complexity without capturing the benefit. It is something I've needed to advocate for and help out with quite a bit.
  • We use something akin to my proposal of a resultFromAsync function in lots and lots of places. I think that should be part of neverthrow and find it pretty difficult to image living without it.
  • We also have a bunch of other utility functions around neverthrow to translate errors to their formats in tRPC, Remix, and Fastify.
  • I think that JavaScript/TypeScript‘s poor ergonomics around error handling are hard to fix with an add-on library and I hope a language-standard method arrives eventually.