To Throw or Not to Throw in TypeScript?

Taking a bit of inspiration from Rust and ending up with much more reliable TypeScript.

How I like to structure error handling in my TypeScript code: do what Rust does. What I mean by that is:

Idiomatic Rust APIs that are fallible by nature return Result, and panic! is reserved for cases where the thing which went wrong is not normal” for that API. For example: std::fs::read returns a Result<Vec<u8>, std::io::Error>, because the file you are attempting to read from might not even exist!1

Thus, when I am writing TypeScript, expected errors should show up in the type signature for a function. That can be with a dedicated Result type or a simple union type:

function fallibleUsingUnion(): Valid | Invalid;
function fallibleUsingResult(): Result<Valid, Invalid>;

These have slightly different tradeoffs: a Result type can provide a lot of conveniences for working with the two paths, but it usually comes with a tiny bit of extra memory overhad and some increase in package size, but that can sometimes be balanced out by the fact that the return type is consistently a Result instance, which can sometimes help out the optimizer. (If performance is an issue, measure instead of assuming!)

Either way if it’s obvious that a given operation is fallible, that should appear in the type signature, and it should not throw an exception. Exceptions are reserved for exceptional behavior.

You can do this with any number of libraries, including:

  • True Myth, the library with Result and Maybe types a friend and I have maintained for over 7 years now. The tryOr and tryOrElse functions are super handy: they take a function which can throw an error and turn it into one which produces a Result instead.

  • neverthrow, a similar library which has also been around the block a bit (it started 5 years ago), and which currently has a nice ResultAsync type of the sort that I have wanted to build for True Myth for years but have never quite gotten to!

Obviously I am a big fan of True Myth, but I would far rather you use neverthrow than not use anything at all.


Notes

  1. Checking for existence may not save you and so is an antipattern: see TOCTOU. ↩︎