Making exceptions type safe in Typescript

Dhruv Rajvanshi
7 min readFeb 11, 2018

--

EDIT: The ideas presented in this post have been released as a library on npm (https://github.com/dhruvrajvanshi/ts-failable) so check that out.

Many times I’ve wished that I could reason about exceptions in a type safe manner. Like getting a specific field from the exception to handle the error. But in Typescript, exceptions have any type by default. You can throw whatever but when you catch it, it will have any type.

EDIT: Some people have pointed out that this just the Either type. Its true that its equivalent to Either type but its more than that. It gives a way to flatten out nested flatMaps so if you have multiple computations that can fail and each depends on the result of the previous, you’ll have as many levels of nesting as there are failable calls depending on the last one. BTW, here’s another article I wrote that solves the nesting problem for Monads in Typescript by simulating Haskell’s do notation. You can use that too for the same effect but I think the syntax for this method is closer to exceptions and less alien.

The alternative to using exceptions is use discriminated unions to indicate the possibility of an error. Something like this.

// So you like boilerplate like if (result.isError) {...}
// Just use Go :/
type MyResult<T> = {
isError: true,
error: ErrorType
} | {
isError: false,
value: T
}

This will force the user of a MyResult<T> value to check the isError field on the result to access the value field. There’s no way around it which is good. However, this approach has a small problem which is not present when using exceptions. Error propagation.

Consider this. If you have two functions that return MyResult values and you want to call them after each other, with the return value of the first call being used as an argument to the second call, you’ll have to check each of them for errors seperately. This can become tedious in case of multiple failable steps in your computation.

If you had used exceptions, you could have called them like normal functions and the errors thrown by each inner function would have propagated up, essentially “short circuiting” the evaluation if any step failed.

What if I told you that you could have type safety as well as error propagation at the same time?

Checked Exceptions (ignore your gag reflex for now)

Java has this feature that lets you annotate a method’s return type with the types of exceptions it can throw. The caller can either handle the error (using try/catch), or add a throws clause to the return type to indicate that this method too can throw the exception.

What? You ask. After what we’ve been through in Java? Really? To which I say, Typescript is not Java. The only reasonable arguments against Java’s checked exceptions are:

  • Adding a new exception type to a function that’s being called deep in the stack requires changing the signature of all the functions that call it.
  • throws clauses aren’t first class types.

The solution to the first problem is there in Typescript. You can alias a function’s errors by doing

type MyFuncErrors = Error1 | Error2 | Error3

If your function needs to throw a new type of error, you can simply do add it to MyFuncErrors and every function that propagates the function will remain unchanged provided the function’s type contains MyFuncErrors and not its cases (which is agains DRY principle anyway).

The functions that catch MyFuncErrors will have to change and the compiler will tell you that. Adding a new case shoud force you to do that.

The idea

Here’s how it will work.

We make a new type called Failable that is generic over the type of the result it can contain and type type of errors it can throw. Failable<T, E> is returned by a function that can return a value of type T on success, or an error of type E in case of failure.

The type is straightforward enough. If there’s an error, isError is true and there will be an error field, containing the error. As per Typescript rules, you’ll have to check isError field to read the value or error.

Next, we need a way to create these Failable values. Writing out object literals is verbose and doesn’t help with error propagation. Take a look at this signature as this is the most important thing.

This might be a bit hard to read but it captures the entire idea.

So, you create a value of Failable<T, E> by calling the failable function.

To it you pass in a lambda function that does whatever computation it needs and returns an error or value. This lambda function will be passed some arguments by failable that would help in indicating success or failure. For now, ignore the run argument.

Here’s an example that simulates a database call. DB calls can fail so that is encoded in its signature. It either returns a Row or “throws” a DB_ERROR or a USER_DOESNT_EXIST_ERROR.

The signature of getUserRow tells you that it takes a string (query), and can either successfully return a Row or undefined, or, in case of failure, it returns a DB_ERROR object. For creating a Failable, it calls failable with the computation. It gets success and failure functions as arguments. Note the destructuring syntax here. Its not strictly required but makes the API a little nicer.

The good thing here is that the types of success and failure are such that returning something other than the given exception type to failure, or something other than the success type to success is not possible.

You can think of success as return and failure as throw. Calling failure actually throws an exceptions under the wraps to do error propagation. success simply wraps its argument into a Failable object.

To explain sequencing, I made another function for converting a Row object to a User object. This can also fail and give a PARSE_ERROR.

Now comes the fun part. How to we call both of these in sequence without explicit error handling, while making sure that an error on any step will be propagated up the call stack?

Here’s a simple function that uses these two functions to get a user from the database.

As you can see, ther’s no explicit error handling done on each step. If getUserRow fails, the the computation stops at the call site and the error is propagated. run function makes sure that if its argument function returns a Failable value having an error, it is thrown. The failable function makes sure that the exceptions thrown by run are converted into Failable values.

Also, run can only be called with a function returning a Failable whose error type is a subtype of the error type of itself. You can test this by removing | DB_ERROR from the type signature. This will result in a type error on the call to getUserRow. We need to either change the type of getUser to include the thrown DB_ERROR, or we need to “catch” and handle it.

The way we can catch a failable computation is by calling it directly instead of passing it to run. If we call it directly, we get a simple object of Failable type that we can check for error using simple if statements. Again, if isError is true, an error will be present and if its false, a value is returned.

The implementation

If you understand the type and semantics of failable function, its straightforward to implement. I won’t bother explaining much and will just give it to you in code. The most interesting thing about it is the fact that failable needs to differentiate between exceptions thrown by run blocks and other exceptions. It should catch the ones thrown by run and convert them to Failable objects with isError: true and throw the other exceptions as it is. This is done by creating a helper class Failure. run wraps the error value of its computation into this type and throws it. Now, failable can simply check if the error was thrown by run using an instanceof Failure check. AFAIK, making a new class/prototype is the only foolproof way to check if an object was created by a particular function. You obviously won’t expose this Failure class to the outside world so that its instances can only be created by run.

Here’s the implementation.

You can see that success and failure are not strictly required. They just wrap up the result/error into a Failable value. Also, I saw that always creating Failables using these functions localized the type errors better.

Better API

Edit: Someone pointed out on Reddit that you don’t need `run` to take a function that returns a `Failable`. Simply passing the `Failable` will do and would clean up the calls.

One obvious way the API of Failable can be made better is by adding a .map method (and maybe even a .flatMap) on it. So, if you have a value of type Failable<number, SomeError>, you could call .map(x => x.toString()) to get a Failable<string, SomeError>. flatMap isn’t that important because run is kind of does that already. So, yes Failable is a monad…and monads are burritos.

Also, it would also make it way nicer if you implement .mapError method on it so that if you’re calling a function that returns a Failable with a different error type than your context, you can convert it to an error of your given type inside run so you can call it normally.

Final thoughts

I think this is a decent ways to have type safe exception handling in Typescript. Unlike Java, where adding a throws clause makes methods tedious to use, in this way, the throws clause is simply a generic argument to the return type. It is a simple type and it can be aliasesd to avoid cascading of type signature changes when adding a new error. Other than that, checked exceptions weren’t that bad anyway. Java’s checked exceptions occasionally made it tedious to do the right thing (adding throws clause along long chains of calls) which made them such a pain. Otherwise, it was a good idea. Heck no I think of it, Java’s checked exception problem can be easily solved by type aliases and union types.

This way is also similar to Rust’s Result type and ? operator, with worse type errors. This is expected because Rust’s handles ? operator in the compiler. Also, Rust can semi automatically converts error types if there’s an implementation of a Convert trait between those types. Rust is one of the languages that has really good error handling thanks to the ? operator and I hope other languages steal this idea.

I might package this into a library if I get the time. You have the implementation here anyway if you need it.

Standard Disclaimer: Don’t pay too much attention to the actual implementation just understand the type signatures and the meaning of the functions. The implementation might have bugs.

--

--