Handling custom error classes in Typescript

October 29, 2020

One of the things I like from other languages like PHP is how you can throw and catch specific exceptions. Replicating this in JS/TS however requires a little extra work.

Here's a PHP example for reference:

<?php
class MyException extends Exception { }

function myFunction() {
  throw new MyException('some reason');
}

try {
  myFunction();
} catch (MyException $e) {
  // Handle MyException specifically...
} catch (Exception $e) {
  // Handle all other exceptions
}

Now over in JS/TS environments the try/catch block doesn't support handling different instances of errors. Instead we'd need to add if/else or switch conditions inside the body of the catch in order to handle different cases.

However, in Typescript you cannot simply extend Error and expect to be able to use: if (error instanceof MyError) { }... Since Typescript 2.1 extending built-ins like Error, Array and Map may not work as you'd expect.

The workaround for this is to call: Object.setPrototypeOf(this, MyError.prototype) from within your consructor. Like so:

class MyError extends Error {
  constructor(message: string) {
    super(message);
    Object.setPrototypeOf(this, MyError.prototype)
  }
}

Now the following code will work:

const myFunction = () => {
  throw new MyError('some reason');
}

try {
  myFunction();
} catch (error) {
  if (error instanceof MyError) {
    // Handle MyError....
  }
}

Previously you would have received an instance of Error instead of MyError.

This is due to how Typescript handles generating the constructor.

This approach to handling errors can produce a bit of nesting in your catch block, however it allows to handle each error differently. I find this useful when you're handling multiple scenarios where an error could be thrown, in some instances you may wish to ignore it or hide it.

Consider the following:

import axios from 'axios';

class JSONAPIError extends Error {
  constructor(message: string) {
    super(message);
    Object.setPrototypeOf(this, MyError.prototype)
  }

  public static fromAxiosError(error: AxiosError) {
    // Surface the detailed error result from our JSON:API compliant api.
    // This error could be more useful to end users
    // Axios error.message contains the status and statusText which
    // isnt useful to users.
    return new JSONAPIError(error.response?.data?.errors[0].detail ?? error.message);
  }
}

const makeApiCall = async (method, url) => {
  try {
    const response = await axios.request({ method, url });
  } catch(error) {
    if (error.response.status === 500) {
      // Notify your error handling system.
      console.error(error);
      // Throw again with a bett
      throw new Error('Internal server error, please try again or contact support');
    }

    throw JSONAPIError.fromAxiosError(error);
  }
}

try {
  const result = await makeApiCall('GET', 'https://google.com/');
  return result;
} catch(error) {
  // A consistent API for handling errors with half decent messages 😍
  window.alert(error.message);
}

The above example shows us how we can better control error messages that could be displayed to an end user. Furthermore I'm still exposing the same API you'd expect from Error. Axios in this case adds the response property where we can access the response from the call that was made, if our code could throw two different errors we'd have the muddy our application logic with checks if certain properties exist. However, I've tucked this away within a function that can be used for making those api calls. Our client that is rendering the error only needs to care about accessing error.message.