Custom Exceptions in modern js / ts

manuartero

Manuel Artero Anguita 🟨

Posted on November 23, 2023

Custom Exceptions in modern js / ts

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 πŸ’›.

πŸ’– πŸ’ͺ πŸ™… 🚩
manuartero
Manuel Artero Anguita 🟨

Posted on November 23, 2023

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

Sign up to receive the latest update from our blog.

Related