DEV Community

Cover image for Custom Exceptions in modern js / ts
Manuel Artero Anguita 🟨
Manuel Artero Anguita 🟨

Posted on

11

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 💛.

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

Top comments (4)

Collapse
 
daniel-dewa profile image
daniel-dewa • Edited

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
Collapse
 
manuartero profile image
Manuel Artero Anguita 🟨 • Edited

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! 👍

Collapse
 
daniel-dewa profile image
daniel-dewa

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.

Thread Thread
 
manuartero profile image
Manuel Artero Anguita 🟨

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

subjective I know.

I just avoid inheritance like the plague 🐀

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay