Beginner-friendy guide to error handling in TypeScript, Node.js, Express.js API design
Tags: javascript, webdev, tutorial, node
Published: 2021-12-21 22:10:37

## 1. Introduction to the topic

### 1.1. Overview

Error handling is pain. You can get pretty far without handling errors correctly, but the bigger the application, the bigger the problems you’re going to face. To really take your API building to the next level, you should tackle the challenge head-on. Error handling is a broad subject, and it can be done in many ways, depending on the application, technologies and more. It’s one of those things that are easy to understand, but hard to fully grasp.

### 1.2. What we’ll be doing

In this article, we’re going to explain a beginner-friendly way of handling errors in Node.js + Express.js API with TypeScript. We are going to explain what an error is, different types of errors that can crop up and how to handle them in our application. Here are some of the things we’ll be doing in the next chapters:

- learning what “error handling” really is and the types of errors that you’ll encounter
- learning about the Node.js `Error` object and how can we use it
- learning how to create custom error classes and how they can help us develop better APIs and Node applications
- learning about Express middleware and how to use them to handle our errors
- learning how to structure the error information and present it to the consumer and developer

### 1.3. Prerequisites

DISCLAMER! **This article assumes you already know some stuff.** Even though this is beginner-friendly, here’s what you should know to get the most out of this article:

- working knowledge of Node.js
- working knowledge of Express.js (routes, middleware and such)
- basics of TypeScript (and classes!)
- basics of how an API works and is written using Express.js

Okay. We can begin.

## 2. What is error handling and why do you need it?

**So what exactly is “error handling” really?**

Error handling (or exception handling) is the process of responding to the occurrence of errors (anomalous/unwanted behaviour) during the execution of a program. 

**Why do we need error handling?**

Because we want to make bug fixing less painful. It also helps us write cleaner code since all error handling code is centralized, instead of handling errors wherever we think they might crop up. In the end - the code is more organized, you repeat yourself less and it reduces development and maintenance time.

## 3. Types of errors

There are **two main types** of errors that we need to differentiate and handle accordingly. 

### 3.1. **Operational Errors**

Operational errors represent runtime problems. They are not necessarily “bugs”, but are external circumstances that can disrupt the flow of program execution. Even though they're not errors in your code, these situations can (and inevitably will) happen and they need to be handled. Here are some examples:

- An API request fails for some reason (e.g., the server is down or the rate limit is exceeded)
- A database connection cannot be established
- The user sends invalid input data
- system ran out of memory

### 3.2. **Programmer errors**

Programmer errors are the real “bugs” and so, they represent issues in the code itself. As mistakes in the syntax or logic of the program, they can be only resolved by changing the source code. Here are some examples of programmer errors:

- Trying to read a property on an object that is not defined
- passing incorrect parameters in a function
- not catching a rejected promise

## 4. What is a Node error?

Node.js has a built-in object called `Error` that we will use as our base to throw errors. When thrown, it has a set of information that will tell us where the error happened, the type of error and what is the problem. The [Node.js documentation](https://nodejs.org/en/knowledge/errors/what-is-the-error-object/) has a more in-depth explanation.

We can create an error like this:

```c
const error = new Error('Error message');
```

Okay, so we gave it a string parameter which will be the error message. But what else does this `Error` have? Since we’re using typescript, we can check its definition, which will lead us to a typescript `interface`:

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

`Name` and `message` are self-explanatory, while `stack` contains the `name`, `message` and a string describing the point in the code at which the `Error` was instantiated. This stack is actually a series of stack frames (learn more about it [here](https://www.wikiwand.com/en/Call_stack)). Each frame describes a call site within the code that lead to the error being generated. We can `console.log()` the stack,

```tsx
console.log(error.stack)
```

 and see what it can tell us. Here’s an example of an error we get when passing a string as an argument to the `JSON.parse()` function (which will fail, since `JSON.parse()` only takes in JSON data in a string format):


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ch0wazex0x7489t2gca.png)

As we can see, this error is of type *SyntaxError,* with the message “*Unexpected token A in JSON at position 0*”. Underneath, we can see the stack frames. This is valuable information we as a developer can use to debug our code and figure out where the problem is - and fix it.

## 5. Writing custom error classes

### 5.1. Custom error classes

As I mentioned before, we can use the built-in `Error` object, as it gives us valuable information.

However, when writing our API we often need to give our developers and consumers of the API a bit more information, so we can make their (and our) life easier.

To do that, we can write a class that will extend the `Error` class with a bit more data.

 

```tsx
class BaseError extends Error {
  statusCode: number;

  constructor(statusCode: number, message: string) {
    super(message);

    Object.setPrototypeOf(this, new.target.prototype);
    this.name = Error.name;
    this.statusCode = statusCode;
    Error.captureStackTrace(this);
  }
}
```

Here we’re creating a `BaseError` class that extends the `Error` class. The object takes a `statusCode` (HTTP status code we will return to the user) and a `message` (error message, just like when creating Node’s built-in `Error` object).

Now we can use the `BaseError` instead of Node’s `Error` class to add the HTTP status code.

```tsx
// Import the class
import { BaseError } from '../utils/error';

const extendedError = new BaseError(400, 'message');
```

We will use this `BaseError` class as our base for all our custom errors.

Now we can use the `BaseError` class to extend it and create all our custom errors. These depend on our application needs. For example, if we’re going to have authentication endpoints in our API, we can extend the `BaseError` class and create an `AuthenticationError` class like this:

```tsx
class AuthenticationError extends BaseError {}
```

It will use the same constructor as our `BaseError`, but once we use it in our code it will make reading and debugging code much easier.

Now that we know how to extend the `Error` object, we can go a step further. 

A common error we might need is a “*not found*” error. Let’s say we have an endpoint where the user specifies a product ID and we try to fetch it from a database. In case we get no results back for that ID, we want to tell the user that the product was not found.

Since we’re probably going to use the same logic for more than just Products (for example Users, Carts, Locations), let’s make this error reusable. 

Let’s extend the `BaseError` class but now, let’s make the status code default to 404 and put a “property” argument in the constructor:

```tsx
class NotFoundError extends BaseError {
  propertyName: string;

  constructor(propertyName: string) {
    super(404, `Property '${propertyName}' not found.`);

    this.propertyName = propertyName;
  }
}
```

Now when using the `NotFoundError` class, we can just give it the property name, and the object will construct the full message for us (statusCode will default to 404 as you can see from the code).

```tsx
// This is how we can use the error
const notFoundError = new NotFoundError('Product');
```

And this is how it looks when it’s thrown:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ria07evpc71zkotnofyw.png)

Now we can create different errors that suit our needs. Some of the most common examples for an API would be:

- ValidationError (errors you can use when handling incoming user data)
- DatabaseError (errors you can use to inform the user that there’s a problem with communicating with the database)
- AuthenticationError (error you can use to signal to the user there’s an authentication error)

### 5.2. Going a step further

Armed with this knowledge, you can go a step further. Depending on your needs, you can add an `errorCode` to the `BaseError` class, and then use it in some of your custom error classes to make the errors more readable to the consumer.

For example, you can use the error codes in the `AuthenticationError` to tell the consumer the type of auth error. `A01` can mean the user is not verified, while `A02` can mean that the reset password link has expired. 

Think about your application’s needs, and try to make it as simple as possible.

### 5.3. Creating and catching errors in controllers

Now let’s take a look at a sample controller (route function) in Express.js

```tsx
const sampleController = (req: Request, res: Response, next: NextFunction) => {

  res.status(200).json({
    response: 'successfull',
    data: {
      answer: 42
    }
  });
};
```

Let’s try to use our custom error class `NotFoundError`.  Let’s use the next() function to pass our custom error object to the next middleware function that will catch the error and take care of it (don’t worry about it, I’ll explain how to catch errors in a minute).

```tsx
const sampleController = async (req: Request, res: Response, next: NextFunction) => {

	return next(new NotFoundError('Product'))

  res.status(200).json({
    response: 'successfull',
    data: {
      answer: 42
    }
  });
};
```

This will successfully stop the execution of this function and pass the error to the next middleware function. So, this is it?

Not quite. We still need to handle errors we don’t handle through our custom errors.

### 5.4. Unhandled mistakes

For example, let’s say you write a piece of code that passes all syntax checks, but will throw an error at runtime. These mistakes can happen, and they will. How do we handle them?

Let’s say you want to use the `JSON.parse()` function. This function takes in JSON data formated as a string, but you give it a random string. Giving this promise-based function a string will cause it to throw an error! If not handled, it will throw an `UnhandledPromiseRejectionWarning` error.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kr4h4e8br9eyex4scd6u.png)

Well, just wrap your code inside a try/catch block, and pass any errors down the middleware line using `next()` (again, I will explain this soon)!

And this really will work. This is not a bad practice, since all errors resulting from promise-based code will be caught inside the `.catch()` block. This has a downside though, and it’s the fact that your controller files will be full of repeated try/catch blocks, and we don’t want to repeat ourselves. Luckily, we do have another ace up our sleeve.

### 5.5. handleAsync wrapper

Since we don’t want to write our try/catch blocks in every controller (route function), we can write a middleware function that does that once, and then apply it on every controller.

Here’s how it looks:

```tsx
const asyncHandler = (fn: any) => (req: Request, res: Response, next: NextFunction) => Promise.resolve(fn(req, res, next)).catch(next);
```

It may look complicated at first, but it’s just a middleware function that acts as a try/catch block with `next(err)` inside the `catch()`. Now, we can just wrap it around our controllers and that’s it!

```tsx
const sampleController = asyncHandler(async (req: Request, res: Response, next: NextFunction) => {
  JSON.parse('A string');

  res.status(200).json({
    response: 'successfull',
    data: {
      something: 2
    }
  });
});
```

Now, if the same error is thrown, we won’t get an `UnhandledPromiseRejectionWarning`, instead, our error handling code will successfully respond and log the error (once we finish writing it, of course. Here’s how it will look like):


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kzxx0x2r7wmaztijurm.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ywqbdh0mmi87a1eu5r77.png)

## 6. How do I handle errors?

Okay, we learned how to create errors. Now what?

Now we need to figure out how to actually *handle* them.

### 6.1. Express middlewares

An express application is essentially a series of middleware function calls. A middleware function has access to the `request` object, the `response` object, and the `next` middleware function.

Express with route each incoming request through these middlewares, from the first down the chain, until the response is sent to the client. Each middleware function can either pass the request to the next middleware with the next() function, or it can respond to the client and resolve the request.

Learn more about Express middleware [here](https://expressjs.com/en/guide/using-middleware.html).

### 6.2. Catching errors in Express

Express has a special type of middleware function called “Error-handling middleware”. These functions have an extra argument `err`. Every time an error is passed in a `next()` middleware function, Express skips all middleware functions and goes straight to the error-handling ones.

Here’s an example on how to write one:

```tsx
const errorMiddleware = (error: any, req: Request, res: Response, next: NextFunction) => {
  // Do something with the error
  next(error); // pass it to the next function
};
```

### 6.3. What to do with errors

Now that we know how to catch errors, we have to do something with them. In APIs, there are generally two things you should do: respond to the client and log the error.

#### 6.3.1. errorReponse middleware (responding to the client)

Personally, when writing APIs I follow a consistent JSON response structure for successful and failed requests:

```tsx
// Success
{
	"response": "successfull",
	"message": "some message if required",
	"data": {}
}

// Failure
{
	"response": "error",
	  "error": {
	    "type": "type of error",
	    "path": "/path/on/which/it/happened",
	    "statusCode": 404,
	    "message": "Message that describes the situation"
	  }
}
```

And now we’re going to write a middleware that handles the failure part.

```tsx
const errorResponse = (error: any, req: Request, res: Response, next: NextFunction) => {
  const customError: boolean = error.constructor.name === 'NodeError' || error.constructor.name === 'SyntaxError' ? false : true;

  res.status(error.statusCode || 500).json({
    response: 'Error',
    error: {
      type: customError === false ? 'UnhandledError' : error.constructor.name,
      path: req.path,
      statusCode: error.statusCode || 500,
      message: error.message
    }
  });
  next(error);
};
```

Let’s examine the function. We first create the `customError` boolean. We check the [`error.constructor.name`](http://error.constructor.name) property which tells us what type of error we’re dealing with. If `error.constructor.name` is `NodeError` (or some other error we didn’t personally create), we set the boolean to false, otherwise we set it to true. This way we can handle known and unknown errors differently.

Next, we can respond to the client. We use the `res.status()` function to set the HTTP status code and we use the `res.json()` function to send the JSON data to the client. When writing the JSON data, we can use the `customError` boolean to set certain properties. For instance, if the `customError` boolean is false, we will set the error type to ‘UnhandledError’, telling the user we didn’t anticipate this situation, otherwise, we set it to `error.constructor.name`.

Since the `statusCode` property is only available in our custom error objects, we can just return 500 if it’s not available (meaning it’s an unhandled error).

In the end, we use the `next()` function to pass the error to the next middleware.

#### 6.3.2. errorLog middleware (logging the error)

```tsx
const errorLogging = (error: any, req: Request, res: Response, next: NextFunction) => {
  const customError: boolean = error.constructor.name === 'NodeError' || error.constructor.name === 'SyntaxError' ? false : true;

  console.log('ERROR');
  console.log(`Type: ${error.constructor.name === 'NodeError' ? 'UnhandledError' : error.constructor.name}`);
  console.log('Path: ' + req.path);
  console.log(`Status code: ${error.statusCode || 500}`);
  console.log(error.stack);
};
```

This function follows the same logic as the one before, with a small difference. Since this logging is intended for developers of the API, we also log the stack.

As you can see, this will just `console.log()` the error data to the system console. In most production APIs logging is a bit more advanced, logging to a file, or logging to an API. Since this part of the API building is very application-specific, I didn’t want to dive in too much. Now that you have the data, choose what approach works best for your application and implement your version of logging. If you’re deploying to a cloud-based deploying service like AWS, you will be able to download log files by just using the middleware function above (AWS saves all the `console.log()`s).

## 7. You can handle errors now.

There you go! That should be enough to get you started with handling errors in a TypeScript + Node.js + Express.js API workflow. Note, there’s a lot of room for improvement here. This approach is not the best, nor the fastest, but is pretty straightforward and most importantly, **forgiving**, and quick to iterate and improve as your API project progresses and demands more from your skills. These concepts are crucial and easy to get started with, and I hope you’ve enjoyed my article and learned something new.

Here's a GitHub repository I made so you can get the full picture: (_coming soon_) 

Think I could’ve done something better? Is something not clear? Write it down in the comments.

Anyone else you think would benefit from this? Share it!

Get in touch: [Telegram](https://t.me/valentinkuharic), [Linkedin](https://www.linkedin.com/in/valentin-kuharic-4468a81a4/), [Website](https://www.valentinkuharic.com/)

Thank you 🙂