A functional (programming) approach to error handling in Typescript

Nicolás Baglivo
FASHION CLOUD
Published in
7 min readSep 7, 2021

Typescript and Javascript provide an error handling strategy based on the try/catch syntax which allows the programmer to escape the normal flow of the program in the presence of errors. This way of doing error handling certainly does its job but there are drawbacks that are often just accepted without giving too much thought about it. In this post, I will detail what these drawbacks are and how some ideas from functional programming can help to overcome them.

This is the way Javascript/Typescript lets you handle errors.

error handling in javascript

We all are familiar with this, and it is probably not too different from the way many other languages handle it.

It allows you to specify a happy path in the try block and the sad path, so to speak, in the catch block.

If parseJson throws then validateValues is not executed, if validating the values throws, success is not printed.

All in all, it works pretty well.

This is how this code looks like in a more declarative manner:

In the functional programming world, this is often referred to as a continuation chain, where the continuation of the chain can be interrupted at any point. This interruption is caused by errors.

The code is written differently but the error handling mechanism does not change. The parseValidJson function will throw if any of the functions in the pipeline throws.

validateJson won't be called if parseJson throws.

Problems with this approach

This works just fine but it does come with some drawbacks and things you have to be aware of.

  • Every function can potentially throw an error, and not just an error, any error. There’s no way to know if or what errors can be thrown from looking at the function signature.
  • Unexpected errors can travel a long way until they meet a try/catch block. In fact, they can crash your application.
  • Errors are side effects. They are not part of the return value of your function and they can affect things outside the function that throws the error. This means that this error would behave differently in your application depending on the context 🤯

Happy path, sad path

One way to look at this is that for every function we have, there are 2 possible outcomes, either a value (the happy path) or an error (the sad path). The interesting part is that when using try/catch error handling, the sad part becomes a special path that escapes the normal flow of the program.

Furthermore, the connection is done through a special mechanism that seems rather obscure: As a function client, you don’t get to see that you could get into this sad path without going into the internals of that function.

A different approach

Imagine that we stop treating errors as side effects but as possible outcomes of the function instead.

I named the following function honestParseJson because this function is honest about the fact that it can return an error. It is saying errors are a possible outcome explicitly by making the error one of the possible returned values.

It can even have specific errors. In this example, we don’t only return any error but a ParsingError

With this simple approach, you have solved the problems

  • You know that the function can error by looking at its signature.
  • You can even know what type of error can happen.
  • You got rid of the side effects.
  • You are forced to handle this error or return it. It is more explicit.

By this point, you might have bought the idea and you are ready to change all your codebases to follow this approach. Well, before you do that, let’s talk about all the problems we introduced with this approach: the actual error handling.

Now let’s imagine that we have the honestJsonParser and another honest function that validates the shape of an object, let’s call it validateValues

This is a high price to pay for those benefits. It is not just uglier and cryptic but code will be more complicated and we’ll create more surface for more errors to happen. We lost the ability to have the 2 paths separated, the happy and the sad paths are now entangled.

The Either Monad

What if I told you there’s a way to keep the continuation chain working as before but without having to live with all the problems that typical error handling introduces?

Let me introduce you to Either.

Either is a type that can either hold a value of type A or a value of type B but never both of them at the same time. Typically it is used to represent computations that can fail with an error. We say the return value of this function is either an error or a result.

You can think of Either as having two sides, the success, which is held on the right, and the failure on the left. You can use this to create a continuation chain where the chain will be executed only on the right side and be interrupted when there’s a value on the left side. Here is some code based on Monet.js, a popular Typescript library that offers an implementation of Either.

There’s a small API that you can use to interact with the value hold by Either. In the code above you can see how we use flatMap to apply a function to the value. This function will only be executed if the Either has the right value on it. If the Either holds a left value the function won't be executed.

In the example, we won’t attempt to validate the JSON if the honestParseJson resulted in an error.

If you are being annoyed by the if condition at the bottom, don't worry, you're not alone. We can use the cata function to get rid of that part.

I know what you’re thinking, do I have to wrap every function into another one that will catch errors and wrap results into either? The answer is no, take a look at this:

Here you can see that JSON.parse and validateJson don't really use Either but you can still use them by wrapping them with the Either.fromTry function.

When you’re creating your own functions, you can simply use Either as part of the design of your API. When using external libs you can integrate them in your flow by using things like Either.fromTry.

For more and better examples of how to use Monet.js or other libraries, surf the internet 🏄.

A comparison with Promise.

There’s a chance that the way functions are chained together with Either was familiar to you. This is because it is very similar to how Promise works.

Promise is also a container, but instead of representing the notion of containing one of two possible values, error and success, it represents a future value. You get to interact with the contained value by using then in the same way you'd use flatMap, map, or cata with Either.

Promise has built-in error handling that works pretty much like Either. It allows you to have the continuation chain when the chain is executed only when no errors are produced.

In this silly example we get things from a certain URL, imagine http.get will give us a promise of the things.

If we get no error, mapToArrayStuff will be applied so we get nice stuff out of the things coming from the API. If we get an error then we handle it with the catch method, mapToArrayStuff won't be executed at all, we'll skip that part. In the final then we'll print either "I got an error" or "I got {stuff}".

If things were not be gotten from an async operation, this would be the Either version.

Nothing really prevents you from implementing this also with Promises although it could be unnecessarily confusing to use this asynchronous concept for synchronous operations.

Conclusion

With this approach you have both things, the ability of chaining functions in a way that you preserve the continuation change and the notion of a happy and a sad path while you get rid of some of the nasty problems of traditional try/catch error handling like when errors you did not even know were possible get out of control and crash your application in hard to debug ways.

--

--

Nicolás Baglivo
FASHION CLOUD

Software dev with 13+ yrs, fascinated by team dynamics and psychology to unlock peak team performance.