Functional way for handling exceptions in typescript

lupuszr

Viktor Pelle

Posted on October 20, 2021

Functional way for handling exceptions in typescript

Lets say we have the following simple function:

const even = (a: number) => {
  if (a % 2 === 0) {
    return a
  }
  throw new Error(`value: ${a} is even`);
}
Enter fullscreen mode Exit fullscreen mode

as you can see if the value is even we return the number else we throw an error. The normal way of handling this would be to wrap it inside a try-catch and handle the error. But when you have multiple functions this becomes tedious and error-prone.

In the fp world instead of throwing errors all over the place, we convert them to data. This in typescript would look something like this:

type Try<A> = Success<A> | Failure
Enter fullscreen mode Exit fullscreen mode

where Success is a container that wraps some value of type A and Failure is the alternative one that contains the error.

Let’s try to implement this structure! We will use a mix of OOP and FP, to create nicely chainable data structures. First, we will create an abstract class that will contain the types of our methods that our data structure will define

abstract class Try<A> {
  abstract map<B>(this: Try<A>, fn: (a: A) => B): Try<B>
  abstract chain<B>(this: Try<A>, fn: (a: A) => Try<B>): Try<B>;
}
Enter fullscreen mode Exit fullscreen mode

We defined two abstract methods: map and chain. The map function acts the same as it does in Array with one element. It takes the value and applies a function over it then "magically" it will be packed back into the array.

Basically:

[1].map(x => `${x}`).
Enter fullscreen mode Exit fullscreen mode

In types:

Array<Int> => (fn: (a: Int) => string) => Array<string>
Enter fullscreen mode Exit fullscreen mode

The chain method is a bit more complicated. While the map just takes out the value, applies a function over it, and packs it back, with a chain it is your responsibility to pack it.
Our analog in an array of one element would be the following:

[1].flatMap(x => [`${x * 2}`]).
Enter fullscreen mode Exit fullscreen mode

In types:

Array<Int> => (fn: (a: Int) => Array<string>) => Array<string>
Enter fullscreen mode Exit fullscreen mode

You may ask why is this chain method useful anyway? As its name suggests we will use it to chain together multiple functions.

Before we continue implementing Success we need to introduce the concept of laziness.
The function arguments in javascript are called by value. What does this mean?
For example:

function a(arg) {
   console.log(hi)
}

function b(a, b) {
   throws new Error(Hello From the other side + a + b)
}

a(b(1, 2))
Enter fullscreen mode Exit fullscreen mode

What will happen here? Even if the argument of the function a was not used we still executed the function b and an error was thrown. How can we solve this? What if we would want to call it inside of a?

Here our some helpers that solves our issue:

// pass as thunk
type $<A> = () => A
function $<A>(a: A): $<A>{
  return () => a
}

// make function lazy
function $fn<A extends (...args: any) => any>(a: A): ((...p: Parameters<A>) => $<ReturnType<A>>) {
  return (...p: [Parameters<A>]) => () => {
    return a(...p)
  }
}
Enter fullscreen mode Exit fullscreen mode

Next let us create a Success that extends Try

class Success<A> extends Try<A> {
  value: A
  constructor(a: A) {
    super()
    this.value = a;
  }

  static of<A>(value: A): Try<A> { return new Success(value); }

  map<B>(fn: (a: A) => B): Try<B> {
    const val = this.value;
    const x = $fn(fn)(val);
    return mkTry<B>(x)
  }

  chain<B>(fn: (a: A) => Try<B>): Try<B> {
    return fn(this.value);
  }
}
Enter fullscreen mode Exit fullscreen mode

And Failure:

class Failure<A> extends Try<A> {
  value: Error;
  constructor(e: Error) {
    super();
    this.value = e;
  }

  static of<A>(value: Error): Try<A> { return new Failure(value); }

  map<B>(fn: (a: A) => B): Try<B> {
    return Failure.of(this.value); 
  }

  chain<B>(fn: (a: A) => Try<B>): Try<B> {
    return Failure.of(this.value)
  }
}
Enter fullscreen mode Exit fullscreen mode

Lets make a helper function to make our life easier:

function mkTry<A>(value: $<A>): Try<A> {
    try {
      const a = value();
      return Success.of(a)
    } catch (error) {
      return Failure.of(error as Error)
    }
  }
Enter fullscreen mode Exit fullscreen mode

With this in place now we have a mechanism to safely chain and execute our code.

Example:

// // define a simple function that throws an error
const x = (a: number) => {
  if (a % 2 === 0) {
    return a
  }
  throw new Error(`value: ${a} is even`);
}

const div4 = (a: number) => {
  if (a % 4 === 0) {
    return a
  }
  throw new Error(`value: ${a} is not dividible by 4`);
}

// make it lazy
const lazyX = $fn(x)
const lazyDiv = $fn(div4)

// // wrap inside try, map chain
const a1 = Try.lift(12)
  .chain(a => mkTry(lazyX(a)))
  .map(b => (b * 2) - 4)
  .chain(a => mkTry(lazyDiv(a)));
console.log(a1);
Enter fullscreen mode Exit fullscreen mode

In this example we defined some functions that can throw an error, then we made them lazy and wrapped them with Try using mkTry data constructor.

Hope this helped a bit :) For any question leave a comment.

For full implementation see: https://gist.github.com/lupuszr/f73baa7ef559f77e6e847ba75ed97182

💖 💪 🙅 🚩
lupuszr
Viktor Pelle

Posted on October 20, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related