System design

Python-like Error Handling in TypeScript

It’s no secret that Python is a well-liked language and generally one of the first languages folks pick up. JavaScript and it’s typed counterpart TypeScript, however, have seen a surge in popularity since the introduction of Node.js and Angular.js over 10 years ago, since it allowed companies to hire teams with a single stack and skillset, reducing the complexities of hiring and reducing the costs of building applications.

JavaScript, however, is often lacking in some respect in terms of features. Many times the community releases a feature before it’s officially incorporated in the JavaScript spec. Promises are a great example of this.

One of the biggest issues when writing JavaScript is the lack of handling errors for specific issues. Python for example allows you to listen for multiple types of errors relatively easily.

class MyCustomError(Exception):
    def __init__(self, message="Custom error occurred"):
        self.message = message
        super().__init__(self.message)
  

try:
        # Business logic
        
    except ValueError as ve:
        print("Oops! That was not a valid number.  Try again...")
    except NameError as ne: 
        print("Oops! That name doesn't exist, Try again...")
    except IndexError as ie:
        print("Oops! Nothing exists at that index! Try again...")
    except MyCustomError as ce:
        print("Oops! This is my custom error. Try again...")

As you can see above, Python allows you to extend the error class to create your own custom error and listen for existing error types. This has been baked into Python from day one, however, JavaScript, and hence it’s superset TypeScript offers no such functionality without a bit of setup on our end.

// Extend JavaScript's native error class
class CustomError extends Error {
  constructor() {
    super()
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

// Define async function to throw
async function throwError(): string {
  throw new CustomError("STOP!")
  
  return "GO!"
}

async function main() {
   // Catch can return the error
   const result = await throwError().catch(e => e)
  
   // instanceof can help custom error
   if(result instanceof CustomError) {
        console.log("Custom Error")
   }
}

As you can see here if we run the code in Chrome we get “CustomError”. This is because catch can return a full error or a result. Our inferred type for the return value in TypeScript would look like type Result = Error | CustomError | String .

One of the best practical applications I’ve found for this pattern is for the Axios HTTP request library. Axios returns a custom error class in case of a 4xx or 5xx error.

import axios, {AxiosError} from 'axios'

const result = await axios.get("http://doesnotexist.com").catch(e => e)
if(result instanceof AxiosError) {
  // handle error
}

Since TypeScript is aware that we checked the instance of the error, TypeScript will automatically handle anything after the if statement handling the instance of condition as an AxiosResult.

Interestingly, this is exactly the pattern Nest.js uses for it’s ExceptionFilters. You’ll also find that many of your favorite libraries for example use a custom error class. MongoServerError for example exists in the MongoDB driver. JoseError exists for the popular JWT library Jose. Many other libraries also implement their own error class. When properly utilized, this makes error handling in your application clean, allowing middleware to be used which can help you avoid crashing, avoid complex error handling code, and have a central point for filtering out errors.