Custom Exceptions in modern js / ts
Manuel Artero Anguita 🟨

Manuel Artero Anguita 🟨 @manuartero

About: Father & happily married 👨‍👩‍👧. Senior Software eng. based on 🇪🇸. TV Apps at @Telefonica. React ⚛️ Professor in a master's program. Js 🟨 Ts 🟦

Location:
Madrid, Spain
Joined:
Jan 18, 2022

Custom Exceptions in modern js / ts

Publish Date: Nov 23 '23
12 4

I remember reading that error handling and meaningful logging are the most common forgotten areas for programmers who aim to improve their skills.

Like, these two are really crucial elements, yet, receive minimal attention 🤷‍♂️.

We - me included - prefer the happy path.
I mean, it's the happy 🌈 path.

Lets review Custom Exceptions in modern javascript / typescript.


Starting point.

  • ✅ We have the Error built-in object.

  • ✅ Using the Error() constructor we get an object with 3 properties

(copied from typescript/lib.es5.d.ts):



interface Error {
    name: string;
    message: string;
    stack?: string;
}


Enter fullscreen mode Exit fullscreen mode

screenshot of a clean console where we are checking the rae properties from Error

note: linenumber and filename are exclusive to Mozilla, that's why those aren't defined at lib.es5.d.ts

  • ✅ There are other built-in constructors available like TypeError() or SyntaxError()

  • ✅ key property that differs an Error from a TypeError (for instance) is name:

screenshot of a clean console where we are printing the name from bare errors


Our goal is:

  1. To be able to define custom errors of our domain
  2. So we're able to detect such errors
  3. Access custom properties, extending the defaults like stack

In pseudo-code:



try {
  foo();
} catch(err) {
  if (/* err is from my Domain so it has custom properties */) {
    const value = err. /* ts is able to suggest properties */
  ...
...


Enter fullscreen mode Exit fullscreen mode

Solution.

Let's pretend we've defined that our domain has AuthError for authentication issues and OperationUnavailableError for other logic issues related to our model.

modern Js Solution 🟨.

  • Just a function that creates a regular Error
  • Define the name (remember, this property is used to differentiate among Errors).
  • Define any extra properties


function AuthError(msg) {
  const err = Error(msg);
  err.name = 'AuthError';
  err.userId = getCurrentUserId();
  return err;
}


Enter fullscreen mode Exit fullscreen mode

Raising it:



function authenticate() {
  ...
  throw AuthError('user not authorized')
  ...
}


Enter fullscreen mode Exit fullscreen mode

Check that we are keeping all the default value from built-in Error:

screenshot of a clean console where we are using our new AuthError

And catching it:



try {
  authenticate();
} catch(err) {
  if (err.name === 'AuthError') {
    const { userId } = err;
    ...
  }
  ...
}


Enter fullscreen mode Exit fullscreen mode

Note: I intentionally avoid using the class keyword; it's so Java-ish doesn't it?


Ts Solution 🟦

Repeating the same here , but including type annotations:

First, the error types.



interface AuthError extends Error {
  name: "AuthError";
  userId: string;
}

interface OperationUnavailableError extends Error {
  name: "OperationUnavailableError";
  info: Record<string, unknown>;
}


Enter fullscreen mode Exit fullscreen mode

this is one of those rare cases where i prefer interface to type since we're extending a built-in interface

And the constructor functions:



function AuthError(msg: string) {
  const error = new Error(msg) as AuthError;
  error.name = "AuthError";
  error.userId = getCurrentUserId();
  return error;
}

function OperationUnavailableError(msg: string) {
  const error = new Error(msg) as OperationUnavailableError;
  error.name = "OperationUnavailableError";
  error.info = getOperationInfo();
  return error;
}


Enter fullscreen mode Exit fullscreen mode

Raising it:



function authenticate() {
  ...
  throw AuthError('user not authorized')
  ...
}


Enter fullscreen mode Exit fullscreen mode

and catching them...

🤔

Using Type Guards ❗

Including these type guards will make your custom errors even nicer:

the devil is in the details



function isAuthError(error: Error): error is AuthError {
  return error.name === "AuthError";
}

function isOperationUnavailableError(
  error: Error
): error is OperationUnavailableError {
  return error.name === "OperationUnavailableError";
}


Enter fullscreen mode Exit fullscreen mode

Code examples mixing up the thing:

example code using both the type guard and the custom error

example-2 code using both the type guard and the custom error


My final advice: Don't over-use custom domain errors; too many can lead to a bureaucratic pyramid of definitions.

They are like... Tabasco 🌶️.

A touch of Tabasco can enhance your code, but moderation is key. If you opt for custom domain errors, keep them simple, following the approach presented here.


thanks for reading 💛.

Comments 4 total

  • daniel-dewa
    daniel-dewaFeb 14, 2025

    Why not extend the base Error class?

    E.g.:

    export class AppNotFoundError extends Error {
      status: number;
    
      constructor(message: string) {
        super(message);
        this.name = "AppNotFoundError";
        this.status = 50;
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode
    • Manuel Artero Anguita 🟨
      Manuel Artero Anguita 🟨Feb 27, 2025

      an option for sure! this is a personal mania of avoiding class and using just the actual function.

      The rationale is composition >>>>>>>>>>>>>>>>>>>>>...>>>>>>> inheritance

      and class leads inevitably to extends abstract this protected which –IMO– makes the software harder to read and maintain.

      THIS is a big topic and im trying to preapre a big article on its own actually!


      TL;DR: I –persoanlly– avoid class at any cost (and prefer plain function) but this solution is completely valid! 👍

      • daniel-dewa
        daniel-dewaFeb 27, 2025

        I would think, that this is an example, where inheritance leads to simpler and easier to understand code.

        Especially since the Error type is used as an interface (some might also call it a trait 🦀), which is a cornerstone of composability.

        • Manuel Artero Anguita 🟨
          Manuel Artero Anguita 🟨Feb 28, 2025

          inheritance leads to simpler and easier to understand code. this sentence is just.... 🙅‍♂️

          subjective I know.

          I just avoid inheritance like the plague 🐀

Add comment