I missed Go's if err != nil, so I built errval for TypeScript
The Motivation: Missing Go's Explicit Error Returns
I write mostly TypeScript and some Go. When I switch back from Go, I miss explicit error returns. A function that can fail says so, and I deal with it right there. In TypeScript I get catch (e), where e is unknown, thrown from some layer I forgot could throw.
That frustration led me to write errval: zero dependencies, 1.86 kB minified and gzipped.
Basic Usage
The core pattern is a tuple where the error comes first:
const [err, user] = await getUser(id)
if (err) return fail(err)
user.email // narrowed. no undefined, no `!`
I don't write the error union manually - it's inferred from the calls to fail().
Compile-Time Safety with match
match will not compile if I forget a case:
return match(err, {
NotFound: (e) => respond(404, e.id),
Forbidden: (e) => respond(403, e.reason),
DbError: (e) => respond(503, e.cause.message),
})
Return a new error type from the service and the match in the handler will stop compiling until it has a case for it.
Benchmarks
I benchmarked it on a request handler where half the requests fail, Node 24.16, per request:
neverthrow: 198 nserrval: 226 nstry/catch: 2,623 nsEffect runSync: 3,952 ns
neverthrow is faster than mine. In that test its errors are bare objects with no name and no message, and mine are real instanceof Error objects with both, so I'm happy to leave the gap.
Plain try/catch wins too when nothing fails at all: 195 ns vs my 203.
The large gap is on the failure path, and it comes from error construction:
- Creating an
Errorsubclass took 1,978 ns. - An
errvalerror took 25 ns, because it never runs theErrorconstructor.
Design Choice: Error First, Not Last
I didn't really build it for the speed. It was for the inferred error unions.
The error comes first in the tuple, not last like in Go. With the value first, const [value] = save() compiles and the error disappears. With the error first, the thing you can accidentally drop is the value. It's 0.1 and it's just me.
Availability
It works on Node, Bun and Deno, and the benchmark code is in the repo.
npm install errval- https://github.com/aymaneallaoui/errval
Would you use [err, value] in a TypeScript codebase? Why or why not?
Comments
No comments yet. Start the discussion.