Custom Exceptions in modern js / ts
Tags: javascript, typescript, node, react
Published: 2023-11-23 15:57:40

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). 

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

(copied from `typescript/lib.es5.d.ts`):
```ts
interface Error {
    name: string;
    message: string;
    stack?: string;
}
```

![screenshot of a clean console where we are checking the rae properties from Error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/de99h2cuwxkzovj0tg78.png)

> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8wl0t477v9lv1pktj65r.png)

***

### 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:

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

## 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

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

Raising it: 

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

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wl12rf6g2ymxwfdc18up.png)

And catching it:

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

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

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

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

> 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:

```ts
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;
}
```

Raising it: 

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

and catching them...

🤔

#### Using Type Guards ❗

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

> the devil is in the details

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

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

Code examples mixing up the thing:

![example code using both the type guard and the custom error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tfckknorflihj4n3zpg6.png)


![example-2 code using both the type guard and the custom error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vaecgrge7xg2v2lpfzj5.png)

***

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