Express error handler typescript

Not always everything goes perfectly, you need to expect the unexpected. To prepare for that, we cover Express error handling and incoming data validation.

December 17, 2018

  • 1. TypeScript Express tutorial #1. Middleware, routing, and controllers
  • 2. TypeScript Express tutorial #2. MongoDB, models and environment variables
  • 3. TypeScript Express tutorial #3. Error handling and validating incoming data
  • 4. TypeScript Express tutorial #4. Registering users and authenticating with JWT
  • 5. TypeScript Express tutorial #5. MongoDB relationships between documents
  • 6. TypeScript Express tutorial #6. Basic data processing with MongoDB aggregation
  • 7. TypeScript Express tutorial #7. Relational databases with Postgres and TypeORM
  • 8. TypeScript Express tutorial #8. Types of relationships with Postgres and TypeORM
  • 9. TypeScript Express tutorial #9. The basics of migrations using TypeORM and Postgres
  • 10. TypeScript Express tutorial #10. Testing Express applications
  • 11. TypeScript Express tutorial #11. Node.js Two-Factor Authentication
  • 12. TypeScript Express tutorial #12. Creating a CI/CD pipeline with Travis and Heroku
  • 13. TypeScript Express tutorial #13. Using Mongoose virtuals to populate documents
  • 14. TypeScript Express tutorial #14. Code optimization with Mongoose Lean Queries
  • 15. TypeScript Express tutorial #15. Using PUT vs PATCH in MongoDB with Mongoose

Since not always everything goes perfectly, you need to expect the unexpected. To prepare for that, we cover TypeScript Express error handling and incoming data validation. Just as before, the repository for the tutorial is mwanago/express-typescript. If you find it helpful, feel free to give it a star.

In the previous part of the tutorial, we wrote a handler function to return a post with a specific ID:

private getPostById = (request: express.Request, response: express.Response) => {

  const id = request.params.id;

  this.post.findById(id)

    .then((post) => {

      response.send(post);

    });

}

There is a slight problem with it though. If a post with a particular ID does not exist, its value in the callback function is null. We then send it, resulting in a response with a code 200 OK. It indicates that everything went fine and the user got a document that he requests, but this post is not empty in our database: it just does not exist. There is a big list of status codes that you can use, but in this case, we use 404 Not Found.

The 200 OK code if default when you use the send function. To change that, you need to call the status function, before using send.

private getPostById = (request: express.Request, response: express.Response) => {

  const id = request.params.id;

  this.post.findById(id)

    .then((post) => {

      if (post) {

        response.send(post);

      } else {

        response.status(404).send({ error: ‘Post not found’ });

      }

    });

}

Now when someone tries to access a post that does not exist, he is informed about what went wrong.

express error handling postman

This outcome is good, but we can make our code better. We want to create errors from our route handlers and let the middleware worry about sending them. There is a default error handler built into Express. To use it, you need to call the next function with an argument (other than the string ‘route’).

private getPostById = (request: express.Request, response: express.Response, next: NextFunction) => {

  const id = request.params.id;

  this.post.findById(id)

    .then((post) => {

      if (post) {

        response.send(post);

      } else {

        next(‘Post not found’);

      }

    });

}

Handling the error in this way results in a 500 Internal Server Error and the error page is rendered.

express error handling default postman

If we want to handle it differently, we need to create our Express error handling middleware

Express Error handling middleware

First, let’s create a class that we are going to use to throw errors.

src/exceptions/HttpException.ts

class HttpException extends Error {

  status: number;

  message: string;

  constructor(status: number, message: string) {

    super(message);

    this.status = status;

    this.message = message;

  }

}

export default HttpException;

If  you would like to know more about the Error object and the errors in general, check out Handling errors in JavaScript with try…catch and finally

Defining Express error handling middleware is almost the same as any other middleware, except we use four arguments instead of three, with the error being the additional first argument.

src/middleware/error.middleware.ts

import { NextFunction, Request, Response } from ‘express’;

import HttpException from ‘../exceptions/HttpException’;

function errorMiddleware(error: HttpException, request: Request, response: Response, next: NextFunction) {

  const status = error.status || 500;

  const message = error.message || ‘Something went wrong’;

  response

    .status(status)

    .send({

      status,

      message,

    })

}

export default errorMiddleware;

Since Express runs all the middleware from the first to the last, your error handlers should be at the end of your application stack. If you pass the error to the next function, the framework omits all the other middleware in the chain and skips straight to the error handling middleware which is recognized by the fact that it has four arguments.

The last thing to do is to attach the error handling middleware to our app:

src/app.ts

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

import * as bodyParser from ‘body-parser’;

import * as express from ‘express’;

import * as mongoose from ‘mongoose’;

import Controller from ‘./interfaces/controller.interface’;

import errorMiddleware from ‘./middleware/error.middleware’;

class App {

  public app: express.Application;

  constructor(controllers: Controller[]) {

    this.app = express();

    this.connectToTheDatabase();

    this.initializeMiddlewares();

    this.initializeControllers(controllers);

    this.initializeErrorHandling();

  }

  public listen() {

    this.app.listen(process.env.PORT, () => {

      console.log(`App listening on the port ${process.env.PORT}`);

    });

  }

  private initializeMiddlewares() {

    this.app.use(bodyParser.json());

  }

  private initializeErrorHandling() {

    this.app.use(errorMiddleware);

  }

  private initializeControllers(controllers: Controller[]) {

    controllers.forEach((controller) => {

      this.app.use(‘/’, controller.router);

    });

  }

  private connectToTheDatabase() {

    const {

      MONGO_USER,

      MONGO_PASSWORD,

      MONGO_PATH,

    } = process.env;

    mongoose.connect(`mongodb://${MONGO_USER}:${MONGO_PASSWORD}${MONGO_PATH}`);

  }

}

export default App;

private getPostById = (request: express.Request, response: express.Response, next: express.NextFunction) => {

  const id = request.params.id;

  this.post.findById(id)

    .then((post) => {

      if (post) {

        response.send(post);

      } else {

        next(new HttpException(404, ‘Post not found’));

      }

    });

}

We can use the HttpException in the same manner when a post that the user wants to delete or modify a post that doesn’t exist. That would mean creating the HttpException in the same way. To avoid redundant code, we can prepare an exception just for that situation.

src/exceptions/PostNotFoundException.ts

import HttpException from «./HttpException»;

class PostNotFoundException extends HttpException {

  constructor(id: string) {

    super(404, `Post with id ${id} not found`);

  }

}

export default PostNotFoundException;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

private getPostById = (request: express.Request, response: express.Response, next: express.NextFunction) => {

  const id = request.params.id;

  this.post.findById(id)

    .then((post) => {

      if (post) {

        response.send(post);

      } else {

        next(new PostNotFoundException(id));

      }

    });

}

private modifyPost = (request: express.Request, response: express.Response, next: express.NextFunction) => {

  const id = request.params.id;

  const postData: Post = request.body;

  this.post.findByIdAndUpdate(id, postData, { new: true })

    .then((post) => {

      if(post) {

        response.send(post);

      } else {

        next(new PostNotFoundException(id));

      }

    });

}

private deletePost = (request: express.Request, response: express.Response, next: express.NextFunction) => {

  const id = request.params.id;

  this.post.findByIdAndDelete(id)

    .then((successResponse) => {

      if (successResponse) {

        response.send(200);

      } else {

        next(new PostNotFoundException(id));

      }

    });

}

Validating incoming data

Another thing worth mentioning is validating input data to prevent the users from creating invalid documents in our collections. To do that I use a package called class-validator with an additional middleware.

The first thing to do is to create a data transfer object (DTO) file that carries data between our functions. It contains specification on how should the incoming data look.

src/posts/post.dto.ts

import { IsString } from ‘class-validator’;

class CreatePostDto {

  @IsString()

  public author: string;

  @IsString()

  public content: string;

  @IsString()

  public title: string;

}

export default CreatePostDto;

To use decorators with TypeScript, you need to add 
«experimentalDecorators»: true to your
tsconfig.json

When we got that down, the only thing left is the validation middleware. Since the body of our request is a plain object, we need to transform it into our class first. To do that, I use the class-transformer package.

src/middleware/validation.middleware.ts

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import { plainToClass } from ‘class-transformer’;

import { validate, ValidationError } from ‘class-validator’;

import * as express from ‘express’;

import HttpException from ‘../exceptions/HttpException’;

function validationMiddleware<T>(type: any): express.RequestHandler {

  return (req, res, next) => {

    validate(plainToClass(type, req.body))

      .then((errors: ValidationError[]) => {

        if (errors.length > 0) {

          const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(‘, ‘);

          next(new HttpException(400, message));

        } else {

          next();

        }

      });

  };

}

export default validationMiddleware;

The class-validator package validates the object, and if it finds some errors, the middleware calls the next function with the error details. Since we pass an error into the next function, the Express error middleware that we described above takes care of it. The errors variable keeps an array of errors, each of them having the constraints object with the details. This simple example creates a string of all of the issues.

The 400 Bad Request status code means that there is something wrong with the request that the client sent.

If you need to, you can also pass an array so that it can be easier to iterate over on the frontend

A thing left to do is to attach the middleware:

import validationMiddleware from ‘../middleware/validation.middleware’;

import CreatePostDto from ‘./post.dto’;

private initializeRoutes() {

  this.router.post(this.path, validationMiddleware(CreatePostDto), this.createPost);

}

Since we want it only on some of our endpoints, we attach it straight before the handler functions. In the example above, the middleware validates the data before the 
this.createPost function runs.

Validating the PATCH handler data

It would be great to use that validation in our updating logic too. There is a small catch: in our CreatePostDto class, all fields are required, and we are using HTTP PATCH that allows for updating just some of the properties without passing the rest of them. There is an easy solution for that thanks to the skipMissingProperties option.

src/middleware/validation.middleware.ts

function validationMiddleware<T>(type: any, skipMissingProperties = false): express.RequestHandler {

  return (req, res, next) => {

    validate(plainToClass(type, req.body), { skipMissingProperties })

      .then((errors: ValidationError[]) => {

        if (errors.length > 0) {

          const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(‘, ‘);

          next(new HttpException(400, message));

        } else {

          next();

        }

      });

  };

}

It skips validating all the properties that are missing, so if you are updating just a part of the document, it doesn’t cause an error just because a required property is not present.

private initializeRoutes() {

  this.router.patch(`${this.path}/:id`, validationMiddleware(CreatePostDto, true), this.modifyPost);

  this.router.post(this.path, validationMiddleware(CreatePostDto), this.createPost);

}

Summary

In this article, we’ve covered Typescript Express error handling. To do that we’ve created an Express error handling middleware and used the next function with an additional argument to pass the error to the error handling middleware. Aside from that, we’ve learned how to validate incoming data in our POST and PATCH handlers using the class-validator package. All those new skills will surely be useful, especially because the next part of the course will cover registering users and authentication.

Previous article
TypeScript Express tutorial #2. MongoDB, models and environment variables

Next article
TypeScript Express tutorial #4. Registering users and authenticating with JWT

Error handling is a crucial part of any production-ready application. It’s often neglected when you are exploring project ideas and trying to learn. But you should never forget about it when you launch your project for public use.

Your users deserve to have the best experience and receive useful error messages. And you deserve the peace of mind that your application can handle all kinds of errors.

When you decide to handle errors yourself, you can add useful information to them, such as HTTP response status codes. You can separate critical errors from those caused by users. Doing error handling yourself gives you options like error logging or sending yourself an email.

In this post you will learn about the many parts of error handling, such as the following:

  • catching all types of errors
  • funneling all errors into a single error handler
  • creating your own error-handling middleware
  • setting up a custom error class
  • implementing the error handler to process all errors

To just see the code, visit this demo repository.

Prerequisites

You should have an Express application set up with TypeScript.

These are the dependencies used in this post.

npm i express
npm i @types/express @types/node ts-node-dev typescript --save-dev

Catching Errors

To handle errors, you first must catch them. You could program the best error handler, but it wouldn’t matter if some errors would escape it.

Express can catch all synchronous errors and send them to its error-handling middleware.

To verify, try throwing an error in one of your routes.

router.get('/', (req: Request, res: Response) => {
  throw new Error('This is an error');

  res.json({ status: 'ok' });
});

Visit the / route in your browser and you should see an error.

Error: This is an error
    at /path/to/project/src/routes.ts:6:9
    ...

Express includes the stack trace in the error message you see in your browser. If you set Node environment to production (NODE_ENV=production), Express will hide the stack trace.

However, errors thrown in asynchronous code can go unnoticed by Express.

Catching Errors in Asynchronous Code

As just mentioned, Express doesn’t catch errors thrown in asynchronous code. Unless you are from the future and using Express 5. You can skip this section then.

For those stuck with Express 4, consider the following code that throws an error inside a promise. Something that could happen when reading user data from a database.

const getUserFromDb = () => {
  return new Promise(() => {
    throw new Error('This is an error');
  });
};

router.get('/', async (req: Request, res: Response) => {
  const data = await getUserFromDb();

  res.json({ user: data });
});

If you revisit the / route, the page won’t load and you will need to restart your application.

You need to catch errors in async code yourself. Otherwise Express won’t pass them to the default error-handling middleware. You can use try/catch and the NextFunction to do that.

import { NextFunction, Request, Response } from 'express';

router.get('/', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const data = await getUserFromDb();

    res.json({ user: data });
  } catch (error) {
    next(error);
  }
});

Notice how you pass the caught error to next function. Any argument you pass to next, Express will treat as an error. By calling next(error), you jump from any middleware straight to the error-handling middleware.

The problem with try/catch approach is having to do it tens or hundreds of times as your app grows. It is repetitive, hence error-prone.

You can use a package dedicated to catching async errors for you. One such package is express-async-errors.

Install it as a dependency.

npm i express-async-errors

And import it before you register any router handlers.

// src/routes.ts

import 'express-async-errors';
import { NextFunction, Request, Response, Router } from 'express';

const router = Router();

const getUserFromDb = () => {
  return new Promise(() => {
    throw new Error('This is an error');
  });
};

router.get('/', async (req: Request, res: Response) => {
  const data = await getUserFromDb();

  res.json({ user: data });
});

export default router;

The express-async-errors package will make sure async errors in your routes get caught. And Express will be able to handle these errors.

So far you have learned that Express can catch errors in synchronous code and how to help it catch async errors. What about errors that Express doesn’t notice? You need to deal with unhandled rejections and uncaught exceptions yourself.

Dealing With Unhandled and Uncaught Errors

Sometimes an error goes unnoticed and Express doesn’t handle it. When Node.js encounters such errors, it emits events that you can listen to.

The first event you need to listen to is unhandledRejection. It happens when your code doesn’t handle a rejected Promise with a .catch().

const getUserFromDb = async () => {
  return new Promise((_, reject) => {
    reject('This is an error');
  });
};

router.get('/', async (req: Request, res: Response) => {
  getUserFromDb()
    .then(value => {
      res.json(value);
    })
});

Since this code doesn’t handle rejections from getUserFromDb with a .catch(), an UnhandledPromiseRejection warning shows up in Node.js console.

To fix it, you can register a listener function on the process object that handles the unhandledRejection event.

// src/process.ts

process.on('unhandledRejection', (reason: Error | any) => {
  console.log(`Unhandled Rejection: ${reason.message || reason}`);

  throw new Error(reason.message || reason);
});

Since rejections are used to return errors from Promises, you can throw an error with the given reason from the rejection.

In this case, Express won’t handle the thrown error anyway. You have created an uncaught exception. Any errors in your code base, that Express can’t handle, turn into uncaught exceptions. Luckily, you can catch them errors yourself.

To catch uncaught exceptions, listen to the uncaughtException event by setting up an event listener on the process object.

// src/process.ts

process.on('uncaughtException', (error: Error) => {
  console.log(`Uncaught Exception: ${error.message}`);

  errorHandler.handleError(error);
});

Here you want to funnel the errors into a function that handles them. Later in this post you will learn how to implement the handleError function.

You can crash your application by calling process.exit(1). Set up an automatic restart mechanism when your application exits with a non-zero code.

Don’t forget to import the code that registers the process event listener functions into your app.

// index.ts

import express, { Application } from 'express';
import './src/process';

You learned how to funnel uncaught errors into your own error handler. The errors caught in middleware functions are still handled by Express. If you want to handle them yourself, you need to create your own error-handling middleware.

Creating a Custom Error-Handling Middleware

To override default Express error responses, you need to create your own error-handling middleware.

An error-handling middleware differs from other middleware functions by having 4 parameters — err, req, res, next. Additionally, it has to be the last middleware you set up in your application. For this reason, call app.use(router) after all other app.use() calls in your index.ts. And register the error-handling middleware after registering other route handlers.

Now go ahead and register your custom error-handling middleware.

// src/routes.ts

import { NextFunction, Request, Response, Router } from 'express';
import { errorHandler } from './exceptions/ErrorHandler';

// ... all other routes

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  errorHandler.handleError(err, res);
});

Similarly to how you previously handled uncaught exceptions, you can funnel all errors into errorHandler, which you will get to implement very soon. You should also pass the Response object to the error handler so you can use it to send a response.

You can have more than one error-handling middleware. To pass the error from one to another, call the next function and pass it the error.

// ... all other routes

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // 1. Log the error or send it to a 3rd party error monitoring software
  logger.logError(err);

  next(err);
});

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // 2. Send an email to yourself, or a message somewhere
  messenger.sendErrorMessage(err);

  next(err);
});

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // 3. Lastly, handle the error
  errorHandler.handleError(err, res);
});

The error will flow through the error-handling middleware functions from first to last (1-2-3).

The last error-handling middleware should send a response, so the client’s connection doesn’t hang up. In this case, handleError should send a response through the res argument.

You can also call next(err) in your last error-handling middleware to send it to Express error handler.

Good job, you have learned a lot. You have set up an error-handling middleware that funnels the caught errors into your error handler. Funneling both uncaught and caught errors, you are almost ready to handle them. One last thing you should do is create a custom error class. This will help to determine the severity of an error and what the HTTP response status code should be.

Creating Custom Error Class

You can use a custom error class to differentiate errors from one another. You might want to add data to an error or handle it differently than other types of errors.

One improvement you can do is attaching an HTTP response status code to your errors.

You can use an enum to map status codes to a human readable name. This way you don’t have to remember the numbers.

// exceptions/AppError.ts

export enum HttpCode {
  OK = 200,
  NO_CONTENT = 204,
  BAD_REQUEST = 400,
  UNAUTHORIZED = 401,
  NOT_FOUND = 404,
  INTERNAL_SERVER_ERROR = 500,
}

These are some of the most common status codes. Go ahead and add any other ones your application uses. You can use this MDN article as a reference.

Another improvement is separating critical application errors from those that are expected. For example, validation errors caused by user input are fine. Errors thrown from mistakes made by you, the developer, are bad.

Now go and create your own AppError class that extends the default Error class.

// exceptions/AppError.ts

export enum HttpCode { /*...*/ }

interface AppErrorArgs {
  name?: string;
  httpCode: HttpCode;
  description: string;
  isOperational?: boolean;
}

export class AppError extends Error {
  public readonly name: string;
  public readonly httpCode: HttpCode;
  public readonly isOperational: boolean = true;

  constructor(args: AppErrorArgs) {
    super(args.description);

    Object.setPrototypeOf(this, new.target.prototype);

    this.name = args.name || 'Error';
    this.httpCode = args.httpCode;

    if (args.isOperational !== undefined) {
      this.isOperational = args.isOperational;
    }

    Error.captureStackTrace(this);
  }
}

When you use super in a child class, you call the constructor of parent class. In this case, calling super will trigger Error class constructor, which sets the error’s message property to contain your description.

The required httpCode and description is what your application will return in responses. Optionally, you can give your error a name.

The isOperational property is what determines if this error is a serious mistake. Setting it to true means that the error is normal and the user should receive an explanation what caused it.

Any error that is not operational indicates a problem with your code, which you should investigate and fix.

Using the Custom Error Class

You use your AppError class whenever you want to fail the client’s request. Whether it be because user lacks permissions, their input is invalid, or they are not logged in. Or for any other reason you desire.

import { AppError, HttpCode } from './exceptions/AppError';

router.get('/user/:id', async (req: Request, res: Response) => {
  if (!res.locals.user) {
    throw new AppError({
      httpCode: HttpCode.UNAUTHORIZED,
      description: 'You must be logged in',
    });
  }

  const user = await getUserFromDb();

  if (!user) {
    throw new AppError({
      httpCode: HttpCode.NOT_FOUND,
      description: 'User you are looking for does not exist',
    });
  }

  res.json(user);
});

You are not limited to routes, you can throw AppError anywhere in your code. Remember to set isOperational to false when throwing a critical error.

Your error-handling middleware that you created earlier will catch all errors from your routes. It will then send them to your error handler, which you are now finally going to create.

Creating an Error Handler

Your error handler should distinguish errors that can be trusted. A trusted error doesn’t take much work, you just have to send an error response to the client. On the other hand, an error you can’t trust requires extra steps.

Start by creating an ErrorHandler class that can determine if an error can be trusted.

// exceptions/ErrorHandler.ts

import { Response } from 'express';
import { AppError, HttpCode } from './AppError';

class ErrorHandler {
  private isTrustedError(error: Error): boolean {
    if (error instanceof AppError) {
      return error.isOperational;
    }

    return false;
  }
}

export const errorHandler = new ErrorHandler();

You can’t trust any other error than your custom AppError. On top of that, if you set isOperational to false when throwing an AppError, you can’t trust such error either.

Now you should handle trustworthy errors coming into your error handler separately from the rest. Check if the error is trustworthy and send it to its dedicated function. Otherwise send it to a function for critical errors.

class ErrorHandler {
  private isTrustedError(error: Error): boolean { /* ...  */ }

  public handleError(error: Error | AppError, response?: Response): void {
    if (this.isTrustedError(error) && response) {
      this.handleTrustedError(error as AppError, response);
    } else {
      this.handleCriticalError(error, response);
    }
  }
}

If an error is trustworthy, it is an instance of AppError, so you can pass handleTrustedError an argument of error as AppError. Since trusted errors can only come from your error-handling middleware, your error handler always receives the Response object along with the error. So, pass it to handleTrustedError as well.

In case of untrustworthy errors, you will check if the response is defined. Because these errors can come from outside the request-response cycle. For example, when you handle an uncaught exception.

Handle a trusted error by sending the client a response with the HTTP status code and a message.

class ErrorHandler {
  // ...

  private handleTrustedError(error: AppError, response: Response): void {
    response.status(error.httpCode).json({ message: error.message });
  }
}

This is where you could also pass the name property to json(). Naming errors can be useful if you want to translate the error message on the client.

On the other hand, untrustworthy errors are dangerous, because they can make your application behave unexpectedly. Based on Node.js best practices on error handling, you should crash your application when you catch such error.

class ErrorHandler {
  // ...

  private handleCriticalError(error: Error | AppError, response?: Response): void {
    if (response) {
      response
        .status(HttpCode.INTERNAL_SERVER_ERROR)
        .json({ message: 'Internal server error' });
    }

    console.log('Application encountered a critical error. Exiting');
    process.exit(1);
  }
}

Since untrustworthy errors come from outside the error-handling middleware, you should check if Response is available. If it’s defined, send a generic server error message to the client.

This is where you would benefit from an automatic restart setup. If your application exits with a non-zero code, it should restart by itself, so you can crash on critical errors without much downtime.

I have written a post Graceful Shutdown in Express that explains how to create your own exit handler.

Of course, you can do much more than just exiting your application. You can set up error logging or notify yourself via email or a messaging service. You could also improve the shutdown procedure to be graceful, stopping all HTTP connections before the exit.

Summary

You are now knowledgeable of what goes into error handling in Express. Here’s a recap of the things you’ve learned:

  • You need to use express-async-errors, because Express can’t catch async errors on its own.
  • You can catch unhandled rejections and uncaught exceptions by listening to events with process.on.
  • You can customize error handling by making your own error-handling middleware.
  • You should use custom error class to save more information, such as status codes and trustworthiness of the error.
  • You should treat untrustworthy errors seriously and probably just restart your application.

You can see the code in this demo repository.

What is an Error Handler in Express.js?

Error handlers in Express.js are in charge of catching and handing errors that happen during runtime. By default, Express.js provides with a built-in by default.

How Does The Error Handler Work in Express.js?

Whenever there is an error occurring in the server, unless you have a custom error handler, Express.js catches the error and sends a response to the client with the error message using its default error handler. The reason Express.js is eager to catch all errors is not only handling properly the errors, but also to correctly empty out unused resources after the application starts running again.

In other words, Express.js is preventing the server from having memory leaks and properly “garbage collects” objects once an API endpoint request is handled by a function.

Let’s explain using an example. Let’s say we have attached a handler function a simple API endpoint.

    app.get('/car/model', function (req: express.Request, res: express.Response) {
      const car = { model: 'Santa Fe', year: 2010, brand: 'Hyundai' };

      res.send(`The car model is ${car.model}`);    });

Enter fullscreen mode

Exit fullscreen mode

Besides the fact of returning a message with car model, let’s dig a little deeper of what is going on.

We created a car variable which contains a car object. We are allocating memory space every time we create a variable. As soon as we send a response back to the client, we are no longer needing the car variable. However, in theory car variable still uses memory space.

Does it mean we are loading up the memory every time we send a response to the client?

When incorrectly handled, yes. We reduce the memory space by not correctly freeing up that memory space. In other words, this is what we call memory leaks.

But! Express.js is protecting us from suffering memory leak issues by using a default error handler.

What Does the Error Handler Have to Do with Preventing from Suffering Memory Leaks?

In order to understand how error handlers are helping from Express.js application from crashing, we need to first understand how the Node.js engine works.

If you didn’t know, Node.js uses V8 engine. V8 engines provides with a built-in garbage collector to manage memory space which leads into a better performance and takes away that extra work from developers.

If you want to understand how the garbage collection work using the V8 engine in depth, I recommend you to read the article written on the V8 engine website. In short, the V8 engine determines memory as dead or alive, meaning if a memory spaces is used, then it is alive. If it is not longer used, then it is dead. Those with the dead status are eligible for garbage collection.

If we go back to the example of the API endpoint using a simple handler function, you will notice that the function is scoped. Once we send a response to the client, the function and any scoped variables are no longer used, making them eligible under the V8’s garbage collection engine.

In the case we force an error in the handler function, such as:

    app.get('/car/model', function (req: express.Request, res: express.Response) {
      const car = { model: 'Santa Fe', year: 2010, brand: 'Hyundai' };

      // forcing to trigger an error
      car.model();

      res.send(`The car model is ${car.model}`);
    });

Enter fullscreen mode

Exit fullscreen mode

And there were no error handlers, the client would never get a response in first place. Also, the request will “hang” on, meaning that our handler function would be using alive memory. Since this memory is alive, the V8 engine garbage collection would not work, causing memory leaks in the server.

Are There Problems with Using Express.js Default Error Handler?

There are no problems with the default error handler. However, it is important for you to understand some concepts. Let’s say we use our previous code example to manually force to trigger an error. I encourage you to make a request to the endpoint. You should get an error response, which it is expected.

However, this error response is not user friendly. It is rather technical and provides with the stack information about the server, such as the folder structure of the API. This information is beneficial if you are troubleshooting errors during development stages, but dangerous if this were to happen in production environments.

Fortunately, Express.js can prevent displaying any sensitive information from errors in the production stages. To make this work, you should make sure the NODE_ENV var is set to “production” prior to creating the express application.

Why Should I Implement a Custom Error Handler?

There can be different reasons why you would want to implement a custom error handler. For example, there projects that don’t set NODE_ENV as “production” in production stages. This could lead to leaking sensitive information about the system if not properly handled.

There are other projects that require to send a specific error object format whenever some unexpected error occurs.

How to Write a Custom Error Handler in Express.js using TypeScript?

Error handler middleware work very similar to any other middleware. However, there are a few things to take into consideration.

When developing an application-level or a route-level middleware, the handler function expects to receive three arguments: Request , Response , and NextFunction. In the case of developing a custom handler error, the handler function is expected to have four arguments. ErrorRequestHandler , Request , Response , and NextFunction.

Let’s go ahead an create our own custom error handler. In our case, we want to send custom error messages in case we want to provide meaningful information to the client about why the server cannot execute a certain process. One example could be of reading the records of a spreadsheet send by the client containing invalid data type for a specific column. Therefore, our first step is to create a CustomError object.

1. Create CustomError Object

First, create a custom-error.model.ts file in the following folder location: src/api/models. Once it is created, we are going to generate a class and export it.

    export class CustomError {
      message!: string;
      status!: number;
      additionalInfo!: any;

      constructor(message: string, status: number = 500, additionalInfo: any = {}) {
        this.message = message;
        this.status = status;
        this.additionalInfo = additionalInfo
      }
    }

Enter fullscreen mode

Exit fullscreen mode

2. Create Custom Error Handler Middleware

Create a file called error-handler.middleware.ts inside the src/api/middlewares folder. Once created, we are going to add the following logic:

    import { Request, Response, NextFunction } from 'express';
    import { CustomError } from './../models/custom-error.model';

    /**
     * Custom error handler to standardize error objects returned to 
     * the client
     * 
     * @param err Error caught by Express.js
     * @param req Request object provided by Express
     * @param res Response object provided by Express
     * @param next NextFunction function provided by Express
     */
    function handleError(
      err: TypeError | CustomError,
      req: Request,
      res: Response,
      next: NextFunction
    ) {
      let customError = err;

      if (!(err instanceof CustomError)) {
        customError = new CustomError(
          'Oh no, this is embarrasing. We are having troubles my friend'
        );
      }

      // we are not using the next function to prvent from triggering 
      // the default error-handler. However, make sure you are sending a 
      // response to client to prevent memory leaks in case you decide to 
      // NOT use, like in this example, the NextFunction .i.e., next(new Error())
      res.status((customError as CustomError).status).send(customError);
    };

    export default handleError; 

Enter fullscreen mode

Exit fullscreen mode

3. Attach Custom Error Handler as The Last Middleware to Use

Open the index.ts file. Then, import the custom handler and add it to the application as the last middleware. In other words, make sure to use the last app.use for the error handler middleware. If you are following the Learn How to Use TypeScript With Node.js and Express.js series, your index.ts file should look like the following:


import express from 'express';
import compression from 'compression';
import helmet from 'helmet';
import bodyParser from 'body-parser';
import cors from 'cors';
import { generateToken } from './api/utils/jwt.utils';

import routes from './api/routes';

import logger from './api/middlewares/logger.middleware';
import errorHandler from './api/middlewares/error-handler.middleware';

const app = express();
const port = 3000;

// Only generate a token for lower level environments
if (process.env.NODE_ENV !== 'production') {
  console.log('JWT', generateToken());
}

// compresses all the responses
app.use(compression());

// adding set of security middlewares
app.use(helmet());

// parse incoming request body and append data to `req.body`
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// enable all CORS request 
app.use(cors());

// add logger middleware
app.use(logger);

app.use('/api/', routes);

// add custom error handler middleware as the last middleware
app.use(errorHandler);

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
}); 

Enter fullscreen mode

Exit fullscreen mode

4. Test Custom Handler

First, force an error in one of the function handlers for any of your API endpoints. In our case, we are going to modify the getTeams function we have in the teams.controller.ts file.


export const getTeams = (req: Request, res: Response) => {
  const test = {};
  //@ts-ignore
  test.nonExistingMethod();

  res.send(TEAMS);
}; 

Enter fullscreen mode

Exit fullscreen mode

Once, me make a request to PORT/api/teams/ we should get the following error object:


{
    "message": "Oh no, this is embarrasing. We are having troubles my friend",
    "status": 500,
    "additionalInfo": {}
}

Enter fullscreen mode

Exit fullscreen mode

Now, let’s use the CustomError object and throw a CustomError instance inside our function handler instead.


import { Request, Response } from 'express';
import { CustomError } from './../models/custom-error.model';

const TEAMS = [
  { id: 1, name: 'Real Madrid', league: 'La Liga' },
  { id: 2, name: 'Barcelona', league: 'La Liga' },
  { id: 3, name: 'Manchester United', league: 'Premier League' },
  { id: 4, name: 'Liverpool', league: 'Premier League' },
  { id: 5, name: 'Arsenal', league: 'Premier League' },
  { id: 6, name: 'Inter', league: 'Serie A' },
  { id: 7, name: 'Milan', league: 'Serie A' },
  { id: 8, name: 'Juventus', league: 'Serie A' },
];

export const getTeams = (req: Request, res: Response) => {
  throw new CustomError('forgot something?', 400, 'you can do better than that');
  res.send(TEAMS);
};

Enter fullscreen mode

Exit fullscreen mode

The expected error object result should be the following:


{
    "message": "forgot something?",
    "status": 400,
    "additionalInfo": "you can do better than that"
}

Enter fullscreen mode

Exit fullscreen mode

This content originally appeared on DEV Community and was authored by Valentin Kuharic

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 has a more in-depth explanation.

We can create an error like this:

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:

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). Each frame describes a call site within the code that lead to the error being generated. We can console.log() the 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

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.

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.

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

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:

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

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

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

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

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

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:

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!

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

Image description

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.

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:

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:

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

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

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, Linkedin, Website

Thank you 🙂

This content originally appeared on DEV Community and was authored by Valentin Kuharic

express-errorhandlers 🚀

GitHub license
GitHub release
GitHub last commit
Travis CI

NPM

Error handler for expressjs(TypeScript)

Install

It is registered in the npm repository — express-errorhandlers. Please install from the npm command.

npm install --save express-errorhandlers

Demo Server

git clone git@github.com:cam-inc/express-errorhandlers.git
cd express-errorhandlers
npm install
npm start # access url : http://localhost:3000/

Screenshot

Access Brower(html)

image

Access curl(json)

$ curl -v -H 'Accept:application/json' "http://localhost:3000/500?foo=1" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /500?foo=1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept:application/json

< HTTP/1.1 500 Internal Server Error
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 1758
< ETag: W/"6de-25i+IrzWztUq0cBgAq/eBD39P50"
< Date: Tue, 06 Mar 2018 09:57:02 GMT
< Connection: keep-alive

{ [1758 bytes data]
100  1758  100  1758    0     0   183k      0 --:--:-- --:--:-- --:--:--  190k
* Connection #0 to host localhost left intact
{
  "response": {
    "status": 500,
    "message": "Server Error",
    "extra": {},
    "stack": "Error: /500 Server Error!!n    at app.get (/Users/fkei/express-errorhandlers/demo.js:41:8)n    at Layer.handle [as handle_request] (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/layer.js:95:5)n    at next (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/route.js:137:13)n    at Route.dispatch (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/route.js:112:3)n    at Layer.handle [as handle_request] (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/layer.js:95:5)n    at /Users/fkei/express-errorhandlers/node_modules/express/lib/router/index.js:281:22n    at Function.process_params (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/index.js:335:12)n    at next (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/index.js:275:10)n    at urlencodedParser (/Users/fkei/express-errorhandlers/node_modules/body-parser/lib/types/urlencoded.js:91:7)n    at Layer.handle [as handle_request] (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/layer.js:95:5)",
    "extraDebug": {}
  },
  "request": {
    "accessurl": "http://localhost:3000/500?foo=1",
    "headers": {
      "host": "localhost:3000",
      "user-agent": "curl/7.54.0",
      "accept": "application/json"
    },
    "hostname": "localhost",
    "ip": "::1",
    "ips": [],
    "originalUrl": "/500?foo=1",
    "url": "/500?foo=1",
    "path": "/500",
    "httpVersion": "1.1",
    "method": "GET",
    "protocol": "http",
    "params": {},
    "query": {
      "foo": "1"
    }
  }
}

Access curl(text)

$ curl -v -H 'Accept:text/plain' "http://localhost:3000/500?foo=1"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /500?foo=1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept:text/plain

< HTTP/1.1 500 Internal Server Error
< X-Powered-By: Express
< Content-Type: text/plain; charset=utf-8
< Date: Tue, 06 Mar 2018 09:59:19 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
>>>> Request

Access Url : http://localhost:3000/500?foo=1
Headers : {
  &quot;host&quot;: &quot;localhost:3000&quot;,
  &quot;user-agent&quot;: &quot;curl/7.54.0&quot;,
  &quot;accept&quot;: &quot;text/plain&quot;
}
Host name : localhost
IP : ::1
IPs :
Original URL : /500?foo=1
URL : /500?foo=1
Path : /500
HTTP Version : 1.1
HTTP Method : GET
HTTP Protocol : http
HTTP Params : {}
HTTP Querys : {
  &quot;foo&quot;: &quot;1&quot;
}
<<<< Response

Status : 500
Message : Server Error
Extra : {}Error Stack trace : Error: /500 Server Error!!
    at app.get (/Users/fkei/express-errorhandlers/demo.js:41:8)
    at Layer.handle [as handle_request] (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/layer.js:95:5)
    at /Users/fkei/express-errorhandlers/node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/index.js:335:12)
    at next (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/index.js:275:10)
    at urlencodedParser (/Users/fkei/express-errorhandlers/node_modules/body-parser/lib/types/urlencoded.js:91:7)
    at Layer.handle [as handle_request] (/Users/fkei/express-errorhandlers/node_modules/express/lib/router/layer.js:95:5)
* Connection #0 to host localhost left intact
Error Extra debug : {}`

API

const expressErrorhandlers = require('express-errorhandlers');

Handler

Handler containing error class.

const Handler = expressErrorhandlers.Handler;

const err = new Error('Error!!');
const handler = new Handler(err, 500, 'Internal Server Error', {serviceErrorCode: 'A-500-000001'}, {userId: 'fkei'})

Handler constructor

  • Type : (Error: error, Int: status, String: message, Object: extra, Object extraDebug)

Handler properties

  • error
    • Type: Error
    • Description: Detected error instance.
    • Default: new Error();
    • Required: no
  • status
    • Type: Int
    • Description: HTTP Response status code.
    • Default: 500
    • Required: no
  • message
    • Type: String
    • Description: Error message.
    • Default: Server Error
    • Required: no
  • extra
    • Type: Object
    • Description: It is a data area that you can freely use. It is also used in production environments.
    • Default: {}
    • Required: no
  • extraDebug
    • Type: Object
    • Description: It is a data area that you can freely use. Ignored in production environment.
    • Default: {}
    • Required: no

Types of Middleware

Skip OK Handler

We will return the unnecessary request such as favicon.ico etc. with 200 OK.
Used with API Server etc.

Register to expressjs middleware. It is desirable after router.

app.use(expressHandlers.middleware.skipOkHandler(
  paths: ['/favicon.ico', '/robots.txt', 'sitemap.xml'], // Set to skip trailing. (optional)
  fn: (err, req, res, next) => { // It is possible to overwrite HTTP Response processing. (optional)
    res.status(200).end();
  };
));

It can be used alone.

Not Found

404 Not Found process is done simply.

Register to expressjs middleware. It is desirable after router.

app.use(expressHandlers.middleware.notFound(
  message : 'Not Found', // custom response message (optional)
  extra : {...}, // It is a data area that you can freely use. It is also used in production environments. (optional)
  extraDebug: {...}, //It is a data area that you can freely use. Ignored in production environment. (optional)
));

Not Found depends on Error Handler middleware.

Error Handler

Implement common error handler using «express next(error)».

  • Response data supports Content-Type of json, html, plain separately for HTTP Header Accept.
  • It is possible to change the output format by development and production.
  • HTML and TEXT output can use template engine(only pug).
  • For custom processing such as log output, any processing can be executed after all processing is finished. (options: final)

Register

Register to expressjs middleware. It is desirable after router.

app.use(expressHandlers.middleware.errorHandler({
  debug: process.env.NODE_ENV !== 'production', //  (optional)
  templateHTML: path.join(__dirname + 'xxx.pug') or 'template string', // pug template string or pug file path (text/html) (optional)
  templateHTMLOptions: { debug: true }, // pug compile config (text/html) (optional)
  templateTEXT: {...}, // pug template string or pug file path (text/plain) (optional)
  templateTEXTOptions: {...}, // pug compile config (text/plain) (optional)
  status: 500, // default response status code (optional)
  message: 'Internal Server Error' // default error message (optional)
  extra: {...}, // It is a data area that you can freely use. It is also used in production environments. (optional)
  extraDebug: {...}, //It is a data area that you can freely use. Ignored in production environment. (optional)
  final: (req, res, handler) => { // Postprocessing function. (optional)
    console.error('final. error:', handler); // log output
  }
}));
debug off (production)

If debugging is off, only the following data will be returned.

{
  response: {
    status: data.status,
    message: data.message,
    extra: data.extra,
  }
};

Changelog

Detailed changes for each release are documented in the release notes.

Copyright

CA Mobile, Inc. All rights reserved.

LICENSE

MIT LICENSE LICENSE

Обработка ошибок — это боль. Вы можете довольно далеко продвинуться без правильной обработки ошибок, но чем больше приложение, тем с большими проблемами вы столкнетесь. Чтобы действительно вывести вашу разработку API на новый уровень, вам следует взяться за решение этой задачи. Обработка ошибок — это обширная тема, и ее можно выполнять разными способами, в зависимости от приложения, технологий и многого другого. Это одна из тех вещей, которые легко понять, но трудно понять полностью.

Что мы будем делать

В этой статье мы собираемся объяснить удобный для новичков способ обработки ошибок в API Node.js + Express.js с помощью TypeScript. Мы собираемся объяснить, что такое ошибка, различные типы ошибок, которые могут возникнуть, и как их обрабатывать в нашем приложении. Вот некоторые вещи, которыми мы займемся в следующих главах:

  1. узнаем, что на самом деле такое «обработка ошибок» и с какими типами ошибок вы столкнетесь
  2. узнаем об объекте Node.js Error и о том, как его использовать
  3. узнаем, как создавать собственные классы ошибок и как они могут помочь нам в разработке лучших API-интерфейсов и Node-приложений
  4. узнаем о промежуточном программном обеспечении Express и о том, как его использовать для обработки наших ошибок
  5. научимся структурировать информацию об ошибках и представлять ее потребителю и разработчику

Предусловие

Предупреждение! В этой статье предполагается, что вы уже кое-что знаете. Несмотря на то, что это удобно для новичков, вот что вам следует знать, чтобы извлечь из этой статьи максимальную пользу:

  1. рабочее знание Node.js
  2. рабочее знание Express.js (маршруты, промежуточное ПО и т. д.)
  3. основы TypeScript (и классы!)
  4. основы работы API и его написания с использованием Express.js

Хорошо. Мы можем начинать.

Что такое обработка ошибок и зачем она вам нужна?

Так что же такое «обработка ошибок» на самом деле?

Обработка ошибок (или обработка исключений) — это процесс реагирования на возникновение ошибок (аномальное/нежелательное поведение) во время выполнения программы.

Зачем нужна обработка ошибок?

Потому что мы хотим сделать исправление ошибок менее болезненным. Это также помогает нам писать более чистый код, поскольку весь код обработки ошибок централизован, вместо того, чтобы обрабатывать ошибки там, где мы думаем, что они могут возникнуть. В конце концов — код более организован, вы меньше повторяетесь, и это сокращает время разработки и обслуживания.

Типы ошибок

Есть два основных типа ошибок, которые нам необходимо различать и соответственно обрабатывать.

Операционные ошибки

Операционные ошибки представляют собой проблемы во время выполнения. Это не обязательно «ошибки», это внешние обстоятельства, которые могут нарушить ход выполнения программы. Несмотря на то, что они не являются ошибками в вашем коде, эти ситуации могут (и неизбежно будут) возникать, и с ними нужно обращаться. Вот некоторые примеры:

  1. Запрос API не выполняется по какой-либо причине (например, сервер не работает или превышен лимит скорости)
  2. Невозможно установить соединение с базой данных
  3. Пользователь отправляет неверные входные данные
  4. Системе не хватает памяти

Ошибки программиста

Ошибки программиста — это настоящие «ошибки», поэтому они представляют собой проблемы в самом коде. Как ошибки в синтаксисе или логике программы, они могут быть устранены только путем изменения исходного кода. Вот несколько примеров ошибок программиста:

  1. Попытка прочитать свойство объекта, которое не определено
  2. Передача некорректных параметров в функции
  3. не улавливая отвергнутого обещания

Что такое ошибка узла?

В Node.js есть встроенный объект Error который мы будем использовать в качестве основы для выдачи ошибок. При вызове он содержит набор информации, которая сообщает нам, где произошла ошибка, тип ошибки и в чем проблема. В документации Node.js есть более подробное объяснение.

Мы можем создать такую ​​ошибку:

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

Итак, мы дали ему строковый параметр, который будет сообщением об ошибке. Но что еще есть в этом Error? Поскольку мы используем typescript, мы можем проверить его определение, что приведет нас к interface typescript:

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

Name и message не требуют пояснений, а stack содержит namemessage и строку, описывающую точку в коде, в которой был создан  Error. Этот стек на самом деле представляет собой серию стековых фреймов (подробнее о нем можно узнать здесь). Каждый фрейм описывает сайт вызова в коде, который приводит к сгенерированной ошибке. Мы можем вызвать стек console.log(),

и посмотрим, что он может нам сказать. Вот пример ошибки, которую мы получаем при передаче строки в качестве аргумента функции JSON.parse() (которая завершится ошибкой, поскольку JSON.parse() принимает только данные JSON в строковом формате):

Как мы видим, это ошибка типа SyntaxError с сообщением « Неожиданный токен A в JSON в позиции 0 ». Внизу мы видим кадры стека. Это ценная информация, которую мы, как разработчик, можем использовать для отладки нашего кода, определения проблемы и ее устранения.

Написание собственных классов ошибок.

Пользовательские классы ошибок

Как я упоминал ранее, мы можем использовать встроенный объект Error, поскольку он дает нам ценную информацию.

Однако при написании нашего API нам часто требуется предоставить нашим разработчикам и потребителям API немного больше информации, чтобы мы могли облегчить их (и нашу) жизнь.

Для этого мы можем написать класс, который расширит класс Error, добавив в него немного больше данных.

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);
  }
}

Здесь мы создаем класс BaseError, расширяющий этот класс Error. Объект принимает statusCode(код состояния HTTP, который мы вернем пользователю) и message(сообщение об ошибке, как при создании встроенного объекта Node Error).

Теперь мы можем использовать класс Node BaseError вместо класса Error для добавления кода состояния HTTP.

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

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

Мы будем использовать этот класс BaseError в качестве основы для всех наших пользовательских ошибок.

Теперь мы можем использовать класс BaseError, чтобы расширить его и сформировать все наши собственные ошибки. Это зависит от потребностей нашего приложения. Например, если мы собираемся иметь конечные точки аутентификации в нашем API, мы можем расширить класс BaseError и создать класс AuthenticationError:

class AuthenticationError extends BaseError {}

Он будет использовать тот же конструктор, что и наш BaseError, но как только мы используем его в нашем коде, он значительно упростит чтение и отладку кода.

Теперь, когда мы знаем, как расширить объект Error, мы можем сделать еще один шаг.

Типичная ошибка, которая может нам понадобиться, — это ошибка «не найдено». Допустим, у нас есть конечная точка, где пользователь указывает идентификатор продукта, и мы пытаемся получить его из базы данных. В случае, если мы не получим результатов по этому идентификатору, мы хотим сообщить пользователю, что продукт не был найден.

Поскольку мы, вероятно, собираемся использовать ту же логику не только для продуктов (например, для пользователей, карт, местоположений), давайте сделаем эту ошибку повторно используемой.

Давайте расширим класс BaseError, но теперь давайте сделаем код состояния по умолчанию 404 и поместим аргумент «свойство» в конструктор:

class NotFoundError extends BaseError {
  propertyName: string;

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

    this.propertyName = propertyName;
  }
}

Теперь при использовании класса NotFoundError мы можем просто дать ему имя свойства, и объект создаст для нас полное сообщение (statusCode по умолчанию будет 404, как вы можете видеть из кода).

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

А вот как это выглядит при сбросе:

Теперь мы можем создавать различные ошибки в соответствии с нашими потребностями. Вот некоторые из наиболее распространенных примеров API:

  1. ValidationError (ошибки, которые можно использовать при обработке входящих пользовательских данных)
  2. DatabaseError (ошибки, которые вы можете использовать, чтобы сообщить пользователю, что существует проблема с взаимодействием с базой данных)
  3. AuthenticationError (ошибка, которую можно использовать, чтобы сообщить пользователю об ошибке аутентификации)

Идем на шаг дальше

Вооружившись этими знаниями, вы сможете сделать еще один шаг вперед. В зависимости от ваших потребностей вы можете добавить errorCode в класс BaseError, а затем использовать его в некоторых из ваших пользовательских классов ошибок, чтобы сделать ошибки более удобочитаемыми для потребителя.

Например, вы можете использовать коды ошибок в AuthenticationError чтобы сообщить потребителю тип ошибки аутентификации. A01 может означать, что пользователь не проверен, а A02 может означать, что срок действия ссылки для сброса пароля истек.

Подумайте о потребностях вашего приложения и постарайтесь сделать его как можно проще.

Создание и обнаружение ошибок в контроллерах

Теперь давайте посмотрим на образец контроллера (функцию маршрута) в Express.js.

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

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

Попробуем использовать наш собственный класс ошибок NotFoundError. Давайте воспользуемся функцией next (), чтобы передать наш настраиваемый объект ошибки следующей функции промежуточного программного обеспечения, которая перехватит ошибку и позаботится о ней (не беспокойтесь об этом, я объясню, как отлавливать ошибки через минуту).

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

    return next(new NotFoundError('Product'))

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

Это успешно остановит выполнение этой функции и передаст ошибку следующей функции промежуточного программного обеспечения. Так вот оно?

Не совсем. Нам все еще нужно обрабатывать ошибки, которые мы не обрабатываем с помощью наших пользовательских ошибок.

Необработанные ошибки

Например, предположим, что вы пишете фрагмент кода, который проходит все проверки синтаксиса, но выдает ошибку во время выполнения. Эти ошибки могут случиться, и они будут. Как мы с ними справляемся?

Допустим, вы хотите использовать функцию JSON.parse(). Эта функция принимает данные JSON в виде строки, но вы даете ей случайную строку. Если передать этой функции, основанной на обещаниях, строку, она выдаст ошибку! Если не обработать, это вызовет ошибку UnhandledPromiseRejectionWarning.

Что ж, просто оберните свой код в блок try/catch и передайте любые ошибки по строке промежуточного программного обеспечения, используя next()(опять же, я скоро объясню это)!

И это действительно сработает. Это неплохая практика, поскольку все ошибки, возникающие из-за кода, основанного на обещаниях, будут обнаружены внутри блока .catch(). Однако у этого есть обратная сторона, а именно тот факт, что ваши файлы контроллера будут заполнены повторяющимися блоками try/catch, и мы не хотим повторяться. К счастью, у нас есть еще один козырь в рукаве.

Оболочка handleAsync

Поскольку мы не хотим писать наши блоки try/catch в каждом контроллере (функция маршрутизации), мы можем написать функцию промежуточного программного обеспечения, которая выполняет это один раз, а затем применяет ее к каждому контроллеру.

Вот как это выглядит:

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

На первый взгляд это может показаться сложным, но это всего лишь функция промежуточного программного обеспечения, которая действует как блок try /catch с next(err) внутри catch(). Теперь мы можем просто обернуть его вокруг наших контроллеров, и все!

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

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

Теперь, если возникнет такая же ошибка, мы не получим UnhandledPromiseRejectionWarning, вместо этого наш код обработки ошибок успешно ответит и зарегистрирует ошибку (разумеется, после того, как мы закончим ее писать. Вот как это будет выглядеть):

Как мне обрабатывать ошибки?

Хорошо, мы научились формировать ошибки. Что теперь?

Теперь нам нужно выяснить, как на самом деле с ними справиться.

Экспресс промежуточное ПО

Экспресс-приложение, по сути, представляет собой серию вызовов функций промежуточного программного обеспечения. Функция промежуточного программного обеспечения имеет доступ к объекту request, объекту response и функции next промежуточного программного обеспечения.

Экспресс с маршрутизацией каждого входящего запроса через эти промежуточные программы, начиная с первого по цепочке, до тех пор, пока ответ не будет отправлен клиенту. Каждая функция промежуточного программного обеспечения может либо передать запрос следующему промежуточному программному обеспечению с помощью функции next(), либо ответить клиенту и разрешить запрос.

Выявление ошибок в Express

В Express есть специальный тип функции промежуточного программного обеспечения, называемый «промежуточное программное обеспечение для обработки ошибок». У этих функций есть дополнительный аргумент err. Каждый раз, когда в функции промежуточного программного обеспечения next() передается ошибка, Express пропускает все функции промежуточного программного обеспечения и сразу переходит к функциям обработки ошибок.

Вот пример того, как его написать:

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

Что делать с ошибками

Теперь, когда мы знаем, как обнаруживать ошибки, мы должны что-то с ними делать. В API обычно нужно сделать две вещи: ответить клиенту и записать ошибку.

errorReponse промежуточное ПО (ответ клиенту)

Лично при написании API я следую согласованной структуре ответов JSON для успешных и неудачных запросов:

// 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"
      }
}

А теперь мы собираемся написать промежуточное программное обеспечение, которое обрабатывает часть сбоя.

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);
};

Давайте рассмотрим функцию. Сначала мы создаем логическое значение customError. Мы проверяем свойство error.constructor.name, которое сообщает нам, с какой ошибкой мы имеем дело. Если error.constructor.name это NodeError(или какая-то другая ошибка, которую мы не создавали лично), мы устанавливаем логическое значение false, в противном случае мы устанавливаем true. Таким образом, мы можем по-разному обрабатывать известные и неизвестные ошибки.

Далее мы можем ответить клиенту. Мы используем res.status() для установки кода состояния HTTP и используем функцию res.json() для отправки данных JSON клиенту. При записи данных JSON мы можем использовать логическое значение customError для установки определенных свойств. Например, если логическое значение false customError, мы установим тип ошибки UnhandledError, сообщая пользователю, что мы не ожидали этой ситуации, в противном случае мы устанавливаем его на error.constructor.name.

Поскольку свойство statusCode доступно только в наших настраиваемых объектах ошибок, мы можем просто вернуть 500, если оно недоступно (то есть это необработанная ошибка).

В конце концов, мы используем функцию next() для передачи ошибки следующему промежуточному программному обеспечению.

errorLog промежуточное ПО (регистрация ошибки)

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);
};

Эта функция следует той же логике, что и предыдущая, с небольшой разницей. Поскольку это ведение журнала предназначено для разработчиков API, мы также регистрируем стек.

Как вы можете видеть, это будет просто console.log() данные об ошибке в системной консоли. В большинстве производственных API ведение журнала немного более продвинуто, запись в файл или запись в API. Поскольку эта часть построения API очень специфична для конкретного приложения, я не хотел слишком углубляться в нее. Теперь, когда у вас есть данные, выберите, какой подход лучше всего подходит для вашего приложения, и реализуйте свою версию ведения журнала. Если вы развертываетесь в облачной службе развертывания, такой как AWS, вы сможете загружать файлы журналов, просто используя функцию промежуточного программного обеспечения, описанную выше (AWS сохраняет все файлы console.log().

Теперь вы можете обрабатывать ошибки.

Вот так! Этого должно быть достаточно, чтобы вы начали обрабатывать ошибки в рабочем процессе TypeScript + Node.js + Express.js API. Обратите внимание, здесь есть много возможностей для улучшения. Этот подход не является лучшим и не самым быстрым, но он довольно прост и, что наиболее важно, снисходителен, и его быстро можно итерировать и улучшать по мере развития вашего проекта API, требующего большего от ваших навыков. Эти концепции очень важны, и с ними легко начать, и я надеюсь, что вам понравилась моя статья и вы узнали что-то новое.

December 21, 2021 10:10 pm GMT

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 youre 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. Its one of those things that are easy to understand, but hard to fully grasp.

1.2. What well be doing

In this article, were 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 well be doing in the next chapters:

  • learning what error handling really is and the types of errors that youll 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, heres 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 has a more in-depth explanation.

We can create an error like this:

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 were using typescript, we can check its definition, which will lead us to a typescript interface:

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 theErrorwas instantiated. This stack is actually a series of stack frames (learn more about it here). Each frame describes a call site within the code that lead to the error being generated. We can console.log() the stack,

and see what it can tell us. Heres 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

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.

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 were 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 Nodes built-in Error object).

Now we can use the BaseError instead of Nodes Error class to add the HTTP status code.

// Import the classimport { 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 were going to have authentication endpoints in our API, we can extend the BaseError class and create an AuthenticationError class like this:

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. Lets 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 were probably going to use the same logic for more than just Products (for example Users, Carts, Locations), lets make this error reusable.

Lets extend the BaseError class but now, lets make the status code default to 404 and put a property argument in the constructor:

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

// This is how we can use the errorconst notFoundError = new NotFoundError('Product');

And this is how it looks when its thrown:

Image description

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 theres a problem with communicating with the database)
  • AuthenticationError (error you can use to signal to the user theres 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 applications needs, and try to make it as simple as possible.

5.3. Creating and catching errors in controllers

Now lets take a look at a sample controller (route function) in Express.js

const sampleController = (req: Request, res: Response, next: NextFunction) => {  res.status(200).json({    response: 'successfull',    data: {      answer: 42    }  });};

Lets try to use our custom error class NotFoundError. Lets 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 (dont worry about it, Ill explain how to catch errors in a minute).

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 dont handle through our custom errors.

5.4. Unhandled mistakes

For example, lets 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?

Lets 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

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 its the fact that your controller files will be full of repeated try/catch blocks, and we dont want to repeat ourselves. Luckily, we do have another ace up our sleeve.

5.5. handleAsync wrapper

Since we dont 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.

Heres how it looks:

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 its 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 thats it!

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 wont get an UnhandledPromiseRejectionWarning, instead, our error handling code will successfully respond and log the error (once we finish writing it, of course. Heres how it will look like):

Image description

Image description

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.

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.

Heres an example on how to write one:

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:

// 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 were going to write a middleware that handles the failure part.

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);};

Lets examine the function. We first create the customError boolean. We check the error.constructor.name property which tells us what type of error were dealing with. If error.constructor.name is NodeError (or some other error we didnt 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 didnt 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 its not available (meaning its 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)

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 didnt 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 youre 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, theres 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 youve 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 couldve 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, Linkedin, Website

Thank you

Original Link: https://dev.to/valentinkuharic/beginner-friendy-guide-to-error-handling-in-typescript-nodejs-expressjs-api-design-432i

Share this article: 
 
Share on Facebook

View Full Article

Понравилась статья? Поделить с друзьями:
  • Express default error handler
  • Express create error
  • Express async error handler
  • Exprelia ошибка e05
  • Exposure lamp error перевод