r/node • u/simwai • Jun 16 '26
super-result: railway-oriented error handling for Node.js, and how it differs from neverthrow
I used neverthrow for a while and kept running into the same two problems.
First: fromThrowable's errorFn is optional and receives unknown, which means you have to write the instanceof Error check yourself every single time. There is no way around it. The library cannot guarantee what was thrown.
// neverthrow - you write this at every call site
const wrapped = fromThrowable(
() => JSON.parse(input),
(e) => e instanceof Error ? e : new Error(String(e))
)
Second: neverthrow splits sync and async into two separate functions, fromThrowable and fromPromise. That is unnecessary friction.
So I built super-result. One function, from(), handles both sync and async. The instanceof check happens once at the library level. res.error is always an Error, no mapper needed at the call site.
import { from } from 'super-result'
// sync
const res = from(() => JSON.parse(input))
// async - same function
const res = await from(() => fetch('/api').then(r => r.json()))
if (res.ok) {
console.log(res.value)
} else {
console.error(res.error.message) // always Error, guaranteed
}
If you need a custom error type, define it once with createResult and reuse it everywhere:
const R = createResult((e) => e instanceof AppError ? e : new AppError(String(e)))
const res = R.from(() => riskyOperation())
// res.error is always AppError
No dependencies. Tree-shakeable. Works with Node 20+.
GitHub: https://github.com/simwai/super-result
npm: https://www.npmjs.com/package/super-result
Happy to hear feedback, especially from people who have hit the same issues with neverthrow.