Error handling nestjs

We go through features that NestJS provides us with, such as Exception filters and Validation pipes

June 1, 2020

  • 1. API with NestJS #1. Controllers, routing and the module structure
  • 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
  • 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
  • 4. API with NestJS #4. Error handling and data validation
  • 5. API with NestJS #5. Serializing the response with interceptors
  • 6. API with NestJS #6. Looking into dependency injection and modules
  • 7. API with NestJS #7. Creating relationships with Postgres and TypeORM
  • 8. API with NestJS #8. Writing unit tests
  • 9. API with NestJS #9. Testing services and controllers with integration tests
  • 10. API with NestJS #10. Uploading public files to Amazon S3
  • 11. API with NestJS #11. Managing private files with Amazon S3
  • 12. API with NestJS #12. Introduction to Elasticsearch
  • 13. API with NestJS #13. Implementing refresh tokens using JWT
  • 14. API with NestJS #14. Improving performance of our Postgres database with indexes
  • 15. API with NestJS #15. Defining transactions with PostgreSQL and TypeORM
  • 16. API with NestJS #16. Using the array data type with PostgreSQL and TypeORM
  • 17. API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM
  • 18. API with NestJS #18. Exploring the idea of microservices
  • 19. API with NestJS #19. Using RabbitMQ to communicate with microservices
  • 20. API with NestJS #20. Communicating with microservices using the gRPC framework
  • 21. API with NestJS #21. An introduction to CQRS
  • 22. API with NestJS #22. Storing JSON with PostgreSQL and TypeORM
  • 23. API with NestJS #23. Implementing in-memory cache to increase the performance
  • 24. API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
  • 25. API with NestJS #25. Sending scheduled emails with cron and Nodemailer
  • 26. API with NestJS #26. Real-time chat with WebSockets
  • 27. API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
  • 28. API with NestJS #28. Dealing in the N + 1 problem in GraphQL
  • 29. API with NestJS #29. Real-time updates with GraphQL subscriptions
  • 30. API with NestJS #30. Scalar types in GraphQL
  • 31. API with NestJS #31. Two-factor authentication
  • 32. API with NestJS #32. Introduction to Prisma with PostgreSQL
  • 33. API with NestJS #33. Managing PostgreSQL relationships with Prisma
  • 34. API with NestJS #34. Handling CPU-intensive tasks with queues
  • 35. API with NestJS #35. Using server-side sessions instead of JSON Web Tokens
  • 36. API with NestJS #36. Introduction to Stripe with React
  • 37. API with NestJS #37. Using Stripe to save credit cards for future use
  • 38. API with NestJS #38. Setting up recurring payments via subscriptions with Stripe
  • 39. API with NestJS #39. Reacting to Stripe events with webhooks
  • 40. API with NestJS #40. Confirming the email address
  • 41. API with NestJS #41. Verifying phone numbers and sending SMS messages with Twilio
  • 42. API with NestJS #42. Authenticating users with Google
  • 43. API with NestJS #43. Introduction to MongoDB
  • 44. API with NestJS #44. Implementing relationships with MongoDB
  • 45. API with NestJS #45. Virtual properties with MongoDB and Mongoose
  • 46. API with NestJS #46. Managing transactions with MongoDB and Mongoose
  • 47. API with NestJS #47. Implementing pagination with MongoDB and Mongoose
  • 48. API with NestJS #48. Definining indexes with MongoDB and Mongoose
  • 49. API with NestJS #49. Updating with PUT and PATCH with MongoDB and Mongoose
  • 50. API with NestJS #50. Introduction to logging with the built-in logger and TypeORM
  • 51. API with NestJS #51. Health checks with Terminus and Datadog
  • 52. API with NestJS #52. Generating documentation with Compodoc and JSDoc
  • 53. API with NestJS #53. Implementing soft deletes with PostgreSQL and TypeORM
  • 54. API with NestJS #54. Storing files inside a PostgreSQL database
  • 55. API with NestJS #55. Uploading files to the server
  • 56. API with NestJS #56. Authorization with roles and claims
  • 57. API with NestJS #57. Composing classes with the mixin pattern
  • 58. API with NestJS #58. Using ETag to implement cache and save bandwidth
  • 59. API with NestJS #59. Introduction to a monorepo with Lerna and Yarn workspaces
  • 60. API with NestJS #60. The OpenAPI specification and Swagger
  • 61. API with NestJS #61. Dealing with circular dependencies
  • 62. API with NestJS #62. Introduction to MikroORM with PostgreSQL
  • 63. API with NestJS #63. Relationships with PostgreSQL and MikroORM
  • 64. API with NestJS #64. Transactions with PostgreSQL and MikroORM
  • 65. API with NestJS #65. Implementing soft deletes using MikroORM and filters
  • 66. API with NestJS #66. Improving PostgreSQL performance with indexes using MikroORM
  • 67. API with NestJS #67. Migrating to TypeORM 0.3
  • 68. API with NestJS #68. Interacting with the application through REPL
  • 69. API with NestJS #69. Database migrations with TypeORM
  • 70. API with NestJS #70. Defining dynamic modules
  • 71. API with NestJS #71. Introduction to feature flags
  • 72. API with NestJS #72. Working with PostgreSQL using raw SQL queries
  • 73. API with NestJS #73. One-to-one relationships with raw SQL queries
  • 74. API with NestJS #74. Designing many-to-one relationships using raw SQL queries
  • 75. API with NestJS #75. Many-to-many relationships using raw SQL queries
  • 76. API with NestJS #76. Working with transactions using raw SQL queries
  • 77. API with NestJS #77. Offset and keyset pagination with raw SQL queries
  • 78. API with NestJS #78. Generating statistics using aggregate functions in raw SQL
  • 79. API with NestJS #79. Implementing searching with pattern matching and raw SQL
  • 80. API with NestJS #80. Updating entities with PUT and PATCH using raw SQL queries
  • 81. API with NestJS #81. Soft deletes with raw SQL queries
  • 82. API with NestJS #82. Introduction to indexes with raw SQL queries
  • 83. API with NestJS #83. Text search with tsvector and raw SQL
  • 84. API with NestJS #84. Implementing filtering using subqueries with raw SQL
  • 85. API with NestJS #85. Defining constraints with raw SQL
  • 86. API with NestJS #86. Logging with the built-in logger when using raw SQL
  • 87. API with NestJS #87. Writing unit tests in a project with raw SQL
  • 88. API with NestJS #88. Testing a project with raw SQL using integration tests
  • 89. API with NestJS #89. Replacing Express with Fastify
  • 90. API with NestJS #90. Using various types of SQL joins
  • 91. API with NestJS #91. Dockerizing a NestJS API with Docker Compose
  • 92. API with NestJS #92. Increasing the developer experience with Docker Compose
  • 93. API with NestJS #93. Deploying a NestJS app with Amazon ECS and RDS
  • 94. API with NestJS #94. Deploying multiple instances on AWS with a load balancer

NestJS shines when it comes to handling errors and validating data. A lot of that is thanks to using decorators. In this article, we go through features that NestJS provides us with, such as Exception filters and Validation pipes.

The code from this series results in this repository. It aims to be an extended version of the official Nest framework TypeScript starter.

Exception filters

Nest has an exception filter that takes care of handling the errors in our application. Whenever we don’t handle an exception ourselves, the exception filter does it for us. It processes the exception and sends it in the response in a user-friendly format.

The default exception filter is called 
BaseExceptionFilter. We can look into the source code of NestJS and inspect its behavior.

nest/packages/core/exceptions/base-exception-filter.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

export class BaseExceptionFilter<T = any> implements ExceptionFilter<T> {

  // …

  catch(exception: T, host: ArgumentsHost) {

    // …

    if (!(exception instanceof HttpException)) {

      return this.handleUnknownError(exception, host, applicationRef);

    }

    const res = exception.getResponse();

    const message = isObject(res)

      ? res

      : {

          statusCode: exception.getStatus(),

          message: res,

        };

    // …

  }

  public handleUnknownError(

    exception: T,

    host: ArgumentsHost,

    applicationRef: AbstractHttpAdapter | HttpServer,

  ) {

    const body = {

      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,

      message: MESSAGES.UNKNOWN_EXCEPTION_MESSAGE,

    };

    // …

  }

}

Every time there is an error in our application, the 
catch method runs. There are a few essential things we can get from the above code.

HttpException

Nest expects us to use the
HttpException class. If we don’t, it interprets the error as unintentional and responds with 500 Internal Server Error.

We’ve used 
HttpException quite a bit in the previous parts of this series:

throw new HttpException(‘Post not found’, HttpStatus.NOT_FOUND);

The constructor takes two required arguments: the response body, and the status code. For the latter, we can use the provided 
HttpStatus enum.

If we provide a string as the definition of the response, NestJS serialized it into an object containing two properties:

  • statusCode: contains the HTTP code that we’ve chosen
  • message: the description that we’ve provided

We can override the above behavior by providing an object as the first argument of the 
HttpException constructor.

We can often find ourselves throwing similar exceptions more than once. To avoid code duplication, we can create custom exceptions. To do so, we need to extend the 
HttpException class.

posts/exception/postNotFund.exception.ts

import { HttpException, HttpStatus } from ‘@nestjs/common’;

class PostNotFoundException extends HttpException {

  constructor(postId: number) {

    super(`Post with id ${postId} not found`, HttpStatus.NOT_FOUND);

  }

}

Our custom 
PostNotFoundException calls the constructor of the  
HttpException. Therefore, we can clean up our code by not having to define the message every time we want to throw an error.

NestJS has a set of exceptions that extend the 
HttpException. One of them is 
NotFoundException. We can refactor the above code and use it.

We can find the full list of built-in HTTP exceptions in the documentation.

posts/exception/postNotFund.exception.ts

import { NotFoundException } from ‘@nestjs/common’;

class PostNotFoundException extends NotFoundException {

  constructor(postId: number) {

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

  }

}

The first argument of the 
NotFoundException class is an additional 
error property. This way, our 
message is defined by 
NotFoundException and is based on the status.

Extending the BaseExceptionFilter

The default 
BaseExceptionFilter can handle most of the regular cases. However, we might want to modify it in some way. The easiest way to do so is to create a filter that extends it.

utils/exceptionsLogger.filter.ts

import { Catch, ArgumentsHost } from ‘@nestjs/common’;

import { BaseExceptionFilter } from ‘@nestjs/core’;

@Catch()

export class ExceptionsLoggerFilter extends BaseExceptionFilter {

  catch(exception: unknown, host: ArgumentsHost) {

    console.log(‘Exception thrown’, exception);

    super.catch(exception, host);

  }

}

The 
@Catch() decorator means that we want our filter to catch all exceptions. We can provide it with a single exception type or a list.

The ArgumentsHost hives us access to the execution context of the application. We explore it in the upcoming parts of this series.

We can use our new filter in three ways. The first one is to use it globally in all our routes through
app.useGlobalFilters.

main.ts

import { HttpAdapterHost, NestFactory } from ‘@nestjs/core’;

import { AppModule } from ‘./app.module’;

import * as cookieParser from ‘cookie-parser’;

import { ExceptionsLoggerFilter } from ‘./utils/exceptionsLogger.filter’;

async function bootstrap() {

  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);

  app.useGlobalFilters(new ExceptionsLoggerFilter(httpAdapter));

  app.use(cookieParser());

  await app.listen(3000);

}

bootstrap();

A better way to inject our filter globally is to add it to our
AppModule. Thanks to that, we could inject additional dependencies into our filter.

import { Module } from ‘@nestjs/common’;

import { ExceptionsLoggerFilter } from ‘./utils/exceptionsLogger.filter’;

import { APP_FILTER } from ‘@nestjs/core’;

@Module({

  // …

  providers: [

    {

      provide: APP_FILTER,

      useClass: ExceptionsLoggerFilter,

    },

  ],

})

export class AppModule {}

The third way to bind filters is to attach the 
@UseFilters decorator. We can provide it with a single filter, or a list of them.

@Get(‘:id’)

@UseFilters(ExceptionsLoggerFilter)

getPostById(@Param(‘id’) id: string) {

  return this.postsService.getPostById(Number(id));

}

The above is not the best approach to logging exceptions. NestJS has a built-in Logger that we cover in the upcoming parts of this series.

Implementing the ExceptionFilter interface

If we need a fully customized behavior for errors, we can build our filter from scratch. It needs to implement the 
ExceptionFilter interface. Let’s look into an example:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

import { ExceptionFilter, Catch, ArgumentsHost, NotFoundException } from ‘@nestjs/common’;

import { Request, Response } from ‘express’;

@Catch(NotFoundException)

export class HttpExceptionFilter implements ExceptionFilter {

  catch(exception: NotFoundException, host: ArgumentsHost) {

    const context = host.switchToHttp();

    const response = context.getResponse<Response>();

    const request = context.getRequest<Request>();

    const status = exception.getStatus();

    const message = exception.getMessage();

    response

      .status(status)

      .json({

        message,

        statusCode: status,

        time: new Date().toISOString(),

      });

  }

}

There are a few notable things above. Since we use 
@Catch(NotFoundException), this filter runs only for 
NotFoundException.

The 
host.switchToHttp method returns the 
HttpArgumentsHost object with information about the HTTP context. We explore it a lot in the upcoming parts of this series when discussing the execution context.

We definitely should validate the upcoming data. In the TypeScript Express series, we use the class-validator library. NestJS also incorporates it.

NestJS comes with a set of built-in pipes. Pipes are usually used to either transform the input data or validate it. Today we only use the predefined pipes, but in the upcoming parts of this series, we might look into creating custom ones.

To start validating data, we need the 
ValidationPipe.

main.ts

import { NestFactory } from ‘@nestjs/core’;

import { AppModule } from ‘./app.module’;

import * as cookieParser from ‘cookie-parser’;

import { ValidationPipe } from ‘@nestjs/common’;

async function bootstrap() {

  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe());

  app.use(cookieParser());

  await app.listen(3000);

}

bootstrap();

In the first part of this series, we’ve created Data Transfer Objects. They define the format of the data sent in a request. They are a perfect place to attach validation.

npm install classvalidator classtransformer

For the 
ValidationPipe to work we also need the class-transformer library

auth/dto/register.dto.ts

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import { IsEmail, IsString, IsNotEmpty, MinLength } from ‘class-validator’;

export class RegisterDto {

  @IsEmail()

  email: string;

  @IsString()

  @IsNotEmpty()

  name: string;

  @IsString()

  @IsNotEmpty()

  @MinLength(7)

  password: string;

}

export default RegisterDto;

Thanks to the fact that we use the above 
RegisterDto with the 
@Body() decorator, the
ValidationPipe now checks the data.

@Post(‘register’)

async register(@Body() registrationData: RegisterDto) {

  return this.authenticationService.register(registrationData);

}

There are a lot more decorators that we can use. For a full list, check out the class-validator documentation. You can also create custom validation decorators.

Validating params

We can also use the class-validator library to validate params.

utils/findOneParams.ts

import { IsNumberString } from ‘class-validator’;

class FindOneParams {

  @IsNumberString()

  id: string;

}

@Get(‘:id’)

getPostById(@Param() { id }: FindOneParams) {

  return this.postsService.getPostById(Number(id));

}

Please note that we don’t use 
@Param(‘id’) anymore here. Instead, we destructure the whole params object.

If you use MongoDB instead of Postgres, the 
@IsMongoId() decorator might prove to be useful for you here

Handling PATCH

In the TypeScript Express series, we discuss the difference between the PUT and PATCH methods. Summing it up, PUT replaces an entity, while PATCH applies a partial modification. When performing partial changes, we need to skip missing properties.

The most straightforward way to handle PATCH is to pass 
skipMissingProperties to our 
ValidationPipe.

app.useGlobalPipes(new ValidationPipe({ skipMissingProperties: true }))

Unfortunately, this would skip missing properties in all of our DTOs. We don’t want to do that when posting data. Instead, we could add 
IsOptional to all properties when updating data.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import { IsString, IsNotEmpty, IsNumber, IsOptional } from ‘class-validator’;

export class UpdatePostDto {

  @IsNumber()

  @IsOptional()

  id: number;

  @IsString()

  @IsNotEmpty()

  @IsOptional()

  content: string;

  @IsString()

  @IsNotEmpty()

  @IsOptional()

  title: string;

}

Unfortunately, the above solution is not very clean. There are some solutions provided to override the default behavior of the
ValidationPipe here.

In the upcoming parts of this series we look into how we can implement PUT instead of PATCH

Summary

In this article, we’ve looked into how error handling and validation works in NestJS. Thanks to looking into how the default 
BaseExceptionFilter works under the hood, we now know how to handle various exceptions properly. We know also know how to change the default behavior if there is such a need. We’ve also how to use the 
ValidationPipe and the class-validator library to validate incoming data.

There is still a lot to cover in the NestJS framework, so stay tuned!

Previous article
API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

Next article
API with NestJS #5. Serializing the response with interceptors

Время прочтения
14 мин

Просмотры 12K

Холивар…

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

  • Некоторая… академичность. Разобрано много и интересно, но заканчивается всё стандартным: «ваш выбор зависит от вашей ситуации».

  • Абсолютно отсутствуют упоминания о бюджете. Никто же не будет спорить, что теоретически мерседес лучше, чем восьмёрка по всем показателям кроме.. цены.

Задача этого поста — поделиться выработанным практическим рецептом. В конкретном фреймворке и с конкретными границами применимости. Без претензий на уникальность, универсальность и, тем более, академическую «правильность».


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

Стартовые условия

Язык

Платформа и ЯП, очень сильно влияют на выбор подходов по работе с ошибками.

К примеру, в go не стоит вопрос, использовать ли исключения — там их нет. В функциональных языках, в частности в F#, было бы очень странно не использовать монады или discriminated union’ы (возврат одного из нескольких возможных типов значений), т. к. это там это реализовано очень удобным и естественным образом. В C#, монады тоже можно сделать, но получается намного больше букв. А это не всем нравится, мне например — не очень. Правда, последнее время всё чаще упоминается библиотека https://www.nuget.org/packages/OneOf/, которая фактически добавляет в язык discriminated union’ы.

А к чему нас подталкивает javascript/typescript?… К анархии! Можно много за что ругать JS и вполне по делу, но точно не за отсутствие гибкости.

Скорее уж за сверхгибкость )). В общем, мы вольны делать так, как нам хочется. Но тут есть, небольшая проблема — когда у вас в команде 10 человек и каждый делает как ему хочется.. получается не очень. Даже если каждый подход в отдельности — неплох.

Фреймворк

С nestjs уже интереснее. Выброс исключений из прикладного кода предлагается нам в документации как основной механизм возврата неуспешных ответов. То есть, если взять обычное http приложение, то чтобы клиенту вернулся статус 404 нам надо бросить NotFoundException..

На самом деле, довольно спорная концепция. И это можно обойти, причём разными способами. Убеждённые сторонники монад вполне могут делать что-то такое:

@Controller()
class SomeController {
  @Post()
  do (): Either<SomeResult, SomeError> {
    ...
  }
}

Для этого, правда придётся написать кое-какой обвязочный код, но можно. Мы не стали.

Важно также, что Фреймворк делает практически всё для того, чтобы нам не приходилось заботиться об устойчивости процесса приложения . Nest сам выстраивает для нас «конвейер» обработки запроса и оборачивает всё это в удобный глобальный «try/catch», который ловит всё.

Правда иногда случаются казусы

Например в одной из старых версий nest’а мы столкнулись с тем, что ошибка, вылетевшая из функции переданной в декоратор @Transform() (из пакета class-transformer) почему-то клала приложение насмерть. В версии 7.5.5 это не воспроизводится, но от подобных вещей, конечно никто не застрахован.

Тип приложения

Самое важное. Мы не пишем софт для спутников. Там вряд ли можно было бы себе позволить что-то в стиле «сервис временно недоступен, попробуйте позже». Для нас же — это вполне ожидаемая ситуация. Нежелательная, конечно, но и не фатальная.

Мы пишем веб-сервисы. Есть http-сервисы, есть rpc (на redis и RabbitMQ, смотрим в сторону gRPC), гибридные тоже есть. В любом случае, мы стараемся внутреннюю логику приложения абстрагировать от транспорта, чтобы в любой момент можно было добавить новый.

Мы фокусируемся на том, что у нас есть запрос, есть его обработчик и есть ответ (который иногда void). И мы допускаем, что обработка запроса может по каким-то причинам оказаться неудачной. В этом случае, либо запрос будет повторён (успешно), либо будет зафиксирован и затем исправлен баг.

При таком подходе, важно, чтобы ошибки не могли привести данные в неконсистентное состояние. Помогают нам в этом две вещи:

  • Транзакционность. То есть, либо получилось всё, либо не получилось ничего.

  • Идемпотентность. Повторное выполнение одной и той же команды не ломает и не меняет состояние системы.

Транзакции (особенно распределённые) и идемпотентность выходят за рамки данной статьи. Но во многом, эти вещи являются основой надёжности.

Ближе к делу

Наши принципы обработки ошибок базируются на следующих соглашениях:

Конфигурация приложения

Конфигурация всегда считывается и валидируется только при старте приложения. Соответственно, об отсутствии каких-то переменных окружения или ещё чего-то, мы узнаём сразу, а не когда-нибудь потом. Достигается это вполне тривиально. Каждый модуль приложения, которому требуется конфигурация включает в себя класс примерно такого вида:

@Injectable()
export class SomeModuleConfig {
  public readonly someUrl: URL;
	public readonly someFile: string;
	public readonly someArrayOfNumbers: number[];

  constructor (source: ConfigurationSource) {
    // Бросит ConfigurationException если не удастся распарсить Url. Можно
    // также проверять его доступность, например, при помощи пакета is-reachable
    this.someUrl = source.getUrl('env.SOME_URL');
		// Бросит ConfigurationException если файл не существует или на него нет прав.
		this.someFile = source.getFile('env.SOME_FILE_PATH');
		// Бросит ConfigurationException если там не перечисленные через запятую числа
		this.someArrayOfNumbers = source.getNumbers('env.NUMBERS')
  }
}

Значения из конфигурации считываются только в конструкторах подобных классов и валидируются максимально строго.

Подход к валидации

Мы написали свои валидаторы. Их преимущетсво в том, что мы не только валидируем данные, но в ряде случаев, можем сделать дополнительные проверки (доступность файла или удалённого ресурса, как в примере выше, например).

Однако, вполне можно использовать joi или json-схемы (может ещё есть варианты) — кому что больше нравится.

Неизменным должно быть одно — всё валидируется на старте.

Уровни абстракции

Мы максимально чётко разделяем бизнес-код и инфраструктурный код. И всё инфраструктурное выносим в библиотеки. Более менее очевидно, но всё же приведу пример:

// Задача: скачать файл по ссылке.
const response = await axios.get(url, { responseType: 'stream' });
const { contentType, filename } = this.parseHeaders(response);
const file = createWriteStream(path);
response.data.pipe(file);
file.on('error', reject);
file.on('finish', () => resolve({ contentType, filename, path }));

Такому коду не место не только в бизнес-логике, но вообще в приложении. В нём нет ничего уникального, привязывающего его к какому-то контексту. Ему место в библиотеке, скажем в классе NetworkFile. Вызывающий же код может выглядеть примерно так:

const file: NetworkFile = await NetworkFile.download('https://download.me/please', {
  saveAs: 'path/to/directory'
});

Фактически, мы заворачиваем в подобные переиспользуемые «смысловые» абстракции почти все нативные нодовские вызовы и вызовы сторонних библиотек. Стратегия обработки ошибок в этих обёртках: «поймать -> завернуть -> бросить«. Пример простейшей реализации такого класса:

export class NetworkFile {
	private constructor (
  	public readonly filename: string,
    public readonly path: string,
    public readonly contentType: string,
    public readonly url: string
  ) {}
  
  // В примере выше у нас метод download принимает вторым аргументов объект опций
  // Таким образом мы можем кастомизировать наш класс: он может записывать файл на диск
  // или не записывать, например.
  // Но тут для примера - самая простая реализация.
  public static async download (url: string, path: string): Promise<NetworkFile> {
    return new Promise<NetworkFile>(async (resolve, reject) => {
      try {
      	const response = await axios.get(url, { responseType: 'stream' });
        const { contentType, filename } = this.parseHeaders(response);
        const file = createWriteStream(path);
        response.data.pipe(file);
				// Здесь мы отловим и завернём все ошибки связанную с записью данных в файл.
        file.on('error', reject(new DownloadException(url, error));
        file.on('finish', () => {
        	resolve(new NetworkFile(filename, path, contentType, url));
        })
    	} catch (error) {
        // А здесь, отловим и завернём ошибки связанные с открытием потока или скачиванием
        // файла по сети.
        reject(new DownloadException(url, error))
      }
    });
  }

	private static parseHeaders (
    response: AxiosResponse
  ): { contentType: string, filename: string } {
    const contentType = response.headers['content-type'];
    const contentDisposition = response.headers['content-disposition'];
    const filename = contentDisposition
			// parse - сторонний пакет content-disposition
      ? parse(contentDisposition)?.parameters?.filename as string
      : null;

    if (typeof filename !== 'string') {
      // Создавать здесь специальный тип ошибки нет смысла, т. к. на уровень выше
      // она завернётся в DownloadException.
      throw new Error(`Couldn't parse filename from header: ${contentDisposition}`);
    }
    return { contentType, filename };
  }
}

Promise constructor anti-pattern

Считается не круто использовать new Promise() вообще, и async-коллбэк внутри в частности. Вот и вот — релевантные посты на stackoverflow по этому поводу.

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

Уследить за потоком управления в таком маленьком классе (на самом деле, его боевая версия лишь немногим больше) — не проблема. А в итоге, вызывающий код работает только с одним типом исключений: DownloadException, внутрь которого завёрнута причина, по которой файл скачать не удалось. И причина носит исключительно информативный характер и не влияет на дальнейшую работу приложения, т. к.:

В бизнес-коде нигде не надо писать TRY / CATCH

Серьёзно, о таких вещах, как закрытие дескрипторов и коннектов не должна заботиться бизнес-логика! Если вам прям очень надо написать try / catch в коде приложения, подумайте.. либо вы пишете то, что должно быть вынесено в библиотеку. Либо.. вам придётся объяснить товарищам по команде, почему именно здесь необходимо нарушить правило (хоть и редко, но такое всё же бывает).

Так почему не надо в сервисе ничего ловить? Для начала:

Что мы считаем исключительной ситуацией?

Откровенно говоря, в этом месте мы сломали немало копий. В конце концов, копья кончились, и мы пришли к концепции холивар-agnostic. Зачем нам отвечать на этот провокационный вопрос? В нём очень легко утонуть, причём мы будем не первыми утопленниками )

Наша концепция проста — при возникновении любой ошибки мы, без споров о её исключительности, завершаем работу обработчика. Никакого геройства — никто не пытается спасать положение!

Не смогли считать файл — до свиданья. Не смогли распарсить ответ от стороннего API — до свидания. В базе duplicate key — до свидания. Не можем найти указанную сущность — до свидания. Максимально просто. И механизм throw, даёт нам удобную возможность осуществить этот быстрый выход без написания дополнительного кода.

В основном исключения ругают за две вещи:

  • Плохой перформанс. Нас это не очень волнует, т. к. мы не highload. Если он нас всё же в какой-то момент настигнет, мы, пересмотрим подходы там, где это будет реально критично. Сделаем бенчмарки… Хотя, готов поспорить, оверхед на исключения будет не главной нашей проблемой.

  • Запутывание потока управления программы. Это как оператор goto который уже давно не применяется в высокоуровневых программах. Вот только в нашем случае, goto бывает только в одно место — к выходу. А ранний return из функции — отнюдь не считается анти-паттерном. Напротив — это очень широко используемый способ уменьшить вложенность кода.

Виды ошибок

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

Мы используем 5 типов рантайм-исключений (про конфигурационные уже говорил выше):

abstract class AuthenticationException extends Exception {
  public readonly type = 'authentication';
}

abstract class NotAllowedException extends Exception {
	public readonly type = 'authorization';
}

abstract class NotFoundException extends Exception {
  public readonly type = 'not_found';
}

abstract class ClientException extends Exception {
  public readonly type = 'client';
}

abstract class ServerException extends Exception {
  public readonly type = 'server';
}

Эти классы семантически соответствуют HTTP-кодам 401, 403, 404, 400 и 500. Конечно, это не вся палитра из спецификации, но нам хватает. Благодаря соглашению, что всё, что вылетает из любого места приложения должно быть унаследовано от указанных типов, их легко автоматически замапить на HTTP ответы.

А если не HTTP? Тут надо смотреть конкретный транспорт. К примеру один из используемых у нас вариантов подразумевает получения сообщения из очереди RabbitMQ и отправку ответного сообщения в конце. Для сериализации ответа мы используем.. что-то типа either:

interface Result<T> {
	data?: T;
  error?: Exception
}

Таким образом, есть всё необходимое, чтобы клиентский код мог интерпретировать ошибку принятым у нас способом.

Базовый класс Exception выглядит примерно так:

export abstract class Exception {
  abstract type: string;
  
	constructor (
    public readonly code: number,
    public readonly message: string,
    public readonly inner?: any
  ) {}

	toString (): string {
    // Здесь логика сериализации, работа со стек-трейсами, вложенными ошибками и проч...
  }
}

Коды ошибок

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

  • Бывает такое, что клиентское приложение должно предпринять различные действия в зависимости от пришедшей от сервера ошибки. С кодами мы можем решить это не добавляя новых http статусов и без, прости Господи, парсинга сообщений.

  • Мы сможем автоматически сформировать и поддерживать индексированный справочник ошибок, которым потом будет пользоваться наша служба технической поддержки. Там будет более подробное описание ошибок, с указанием возможных способов их исправления, паролями и явками — куда бежать.

Насколько это всё нужно и полезно — жизнь покажет

Поле inner — это внутренняя ошибка, которая может быть «завёрнута» в исключение (см. пример с NetworkFile).

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

Примеры использования

Опустим AuthenticationException — он используется у нас только в модуле контроля доступа. Разберём более типовые примеры и начнём ошибок валидации:

import { ValidatorError } from 'class-validator';
// ....

export interface RequestValidationError {
  // Массив - потому что ошибка может относиться к нескольким полям.
  properties: string[];
  errors: { [key: string]: string };
	nested?: RequestValidationError[]
}

// Небольшая трансформация стандартной ошибки class-validator'а в более удобный
// "наш" формат.
const mapError = (error: ValidationError): RequestValidationError => ({
  properties: [error.property],
  errors: error.constraints,
  nested: error.children.map(mapError)
});

// Сами цифры не имеют значения.
export const VALIDATION_ERROR_CODE = 4001;

export class ValidationException extends ClientException {
  constructor (errors: ValidationError[]) {
    const projections: ValErrorProjection[] = ;
    super(
      VALIDATION_ERROR_CODE,
      'Validation failed!',
      errors.map(mapError)
    );
  }
}

Иногда валидация производится в пайпе, иногда где-то в обработчике, но мы всегда хотим, чтобы на выходе был единообразный, удобный для нас формат. Заставить стандартный нестовый ValidationPipe выбрасывать то, что нам хочется можно просто передав функцию-фабрику в конструктор:

app.useGlobalPipes(
	new ValidationPipe({
  	exceptionFactory: errors => new ValidationException(errors); 
  });
)

Соответственно, на выходе наш ValidationException замапится на BadRequestException с кодом 400 — потому что он ClientException.

Другой пример, с NotFoundException:

export const EMPLOYEE_NOT_FOUND_ERROR_CODE = 50712;

export class EmployeeNotFoundException extends NotFoundException {
  constructor (employeeId: number) {
  	super(
      EMPLOYEE_NOT_FOUND_ERROR_CODE,
      `Employee id = ${employeeId} not found!`
    );
  }
}

Не очень приятно писать такие классы — всегда возникает лёгкое чувство… бойлерплейта ) Но зато, как приятно их потом использовать!

// Вместо, что не даст нам ни кодов, ни типа - ничего:
throw new Error('...тут мы должны сформировать внятное сообщение...')

// Просто
throw new EmployeeNotFoundException(id);

Сценарий использования NotAllowedException похож на предыдущий. Пользовать может иметь доступ к роуту getEmployeeById, но не иметь права запрашивать определённые категории работников. Соответственно, мы в сервисе можем проверить его доступ и выкинуть ошибку такого вида:

export const EMPLOYEE_NOT_ALLOWED_ERROR_CODE = 40565;

export class EmployeeNotAllowedException extends NotAllowedException {
  constructor (userId: number, employeeId: number) {
  	super(
      EMPLOYEE_NOT_ALLOWED_ERROR_CODE,
      `User id = ${userId} is not allowed to query employee id = ${employeeId}!`
    );
  }
}

Ну и наконец, все ошибки, в которых клиент не виноват: он имеет нужный доступ, запросил существующий ресурс и нигде не напутал с полями, и всё-таки облом — значит ServerException. Здесь я даже не буду особо приводить примеров — их множество. Но для клиента они всё одно — ошибка на сервере.

Маппинг

Мапятся внутренние ошибки на транспортно-специфичные в едином GlobalExceptionFilter, фильтр этот на вход получает один или несколько форматтеров. Задача форматтеров — преобразование вылетевшей ошибки в её конечный вид, можно сказать сериализация.

export interface IExceptionsFormatter {
  // Verbose - флаг, который мы держим в конфигурации. Он используется для того,
  // чтобы в девелоперской среде всегда на клиент отдавалась полная инфа о
  // ошибке, а на проде - нет.
  format (exception: unknown, verbose: boolean): unknown;
  
  // При помощи этого метода можно понять, подходит ли данных форматтер
  // для этого типа приложения или нет.
  match (host: ArgumentsHost): boolean;
}


@Module({})
export class ExceptionsModule {
  public static forRoot (options: ExceptionsModuleOptions): DynamicModule {
    return {
      module: ExceptionsModule,
      providers: [
        ExceptionsModuleConfig,
        {
          provide: APP_FILTER,
          useClass: GlobalExceptionsFilter
        },
        {
          provide: 'FORMATTERS',
          useValue: options.formatters
        }
      ]
    };
  }
}

const typesMap = new Map<string, number>()
	.set('authentication', 401)
	.set('authorization', 403)
	.set('not_found', 404)
	.set('client', 400)
	.set('server', 500);

@Catch()
export class GlobalExceptionsFilter implements ExceptionFilter {
  constructor (
    @InjectLogger(GlobalExceptionsFilter) private readonly logger: ILogger,
    @Inject('FORMATTERS') private readonly formatters: IExceptionsFormatter[],
    private readonly config: ExceptionsModuleConfig
  ) { }

  catch (exception: Exception, argumentsHost: ArgumentsHost): Observable<any> {
    this.logger.error(exception);
    const formatter = this.formatters.find(x => x.match(argumentsHost));
    const payload = formatter?.format(exception, this.config.verbose) || 'NO FORMATTER';
    
		// В случае http мы ставим нужный статус-код и возвращаем ответ.
		if (argumentsHost.getType() === 'http') {
      const request = argumentsHost.switchToHttp().getResponse();
      const status = typesMap.get(exception.type) || 500;
      request.status(status).send(payload);
      return EMPTY;
    }
		// В случае же RPC - бросаем дальше, транспорт разберётся.
    return throwError(payload);
  }
}

Бывает конечно, что мы где-то напортачили и из сервиса вылетело что-то не унаследованное от Exception. На этот случай у нас есть ещё интерцептор, который все ошибки, не являющиеся экземплярами наследников Exception, заворачивает в new UnexpectedException(error) и прокидывает дальше. UnexpectedException естественно наследуется от ServerException. Для нас возникновение такой ошибки — иногда некритичный, но всё же баг, который фиксируется и исправляется.


В принципе, это всё. Для 95% наших задач этого вполне хватает. Способ может и не «канонический», но удобный и вполне рабочий — то, к чему мы и стремились.

И всё же бывают ситуации

Когда не всё так ясно

Приведу два примера:

Первый. Операции допускающие частичный успех. Мы стараемся избегать таких вещей, но как избежать, если нам нужно реализовать загрузку данных из csv-файла а бизнес требует, чтобы при возникновении ошибок в отдельных строках, остальные всё же были загружены?

В таких случаях всё-таки приходится проявлять «фантазию», например, обернуть в try/catch обработку каждой строки csv-файла. И в блоке catch писать ошибку в «отчёт». Тоже не бином Ньютона )

Второй. Я сознательно не написал выше реализацию DownloadException.

export class DOWNLOAD_ERROR_CODE = 5506;

export class DownloadException extends ServerException {
  constructor (url: string, inner: any) {
    super(
      DOWNLOAD_ERROR_CODE,
      `Failed to download file from ${url}`,
      inner
    );
  }
}

Почему ServerException? Потому что, в общем случае, клиенту всё равно почему сервер не смог куда-то там достучаться. Для него это просто какая-то ошибка, в которой он не виноват.

Однако, теоретически может быть такая ситуация, что мы пытаемся скачать файл по ссылке, предоставленной клиентом. И тогда, в случае неудачи, клиент должен получить 400 или может быть 404, но не 500.

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

Заключение

Большое спасибо всем, кто дочитал до конца. Не думаю, что я в этой статье кому-то «открыл Америку». И я, конечно, далёк от навязывания кому-либо, чего-либо. И всё же, надеюсь, что эта попытка структурировать работу с ошибками для кого-то послужит отправной точкой в разработке собственного подхода под собственные задачи.

P. S. К сожалению, опенсорс в нашей компании в процессе согласования, поэтому привести реальный используемый код я не могу. Когда будет такая возможность, мы выложим на github библиотеку, при помощи которой работаем с исключениями. А за одно и некоторые другие пакеты, которые могут оказаться кому-то полезными.

P. P. S. Буду очень благодарен, за комментарии, особенно если в них будут практические примеры ситуаций, в которых у нас могут возникнуть трудности при использовании данного подхода. С удовольствием, поучаствую в обсуждении.

While using NestJS to create API’s I was wondering which is the best way to handle errors/exception.
I have found two different approaches :

  1. Have individual services and validation pipes throw new Error(), have the controller catch them and then throw the appropriate kind of HttpException(BadRequestException, ForbiddenException etc..)
  2. Have the controller simply call the service/validation pipe method responsible for handling that part of business logic, and throw the appropriate HttpException.

There are pros and cons to both approaches:

  1. This seems the right way, however, the service can return Error for different reasons, how do I know from the controller which would be the corresponding kind of HttpException to return?
  2. Very flexible, but having Http related stuff in services just seems wrong.

I was wondering, which one (if any) is the «nest js» way of doing it?

How do you handle this matter?

frederj's user avatar

frederj

1,4359 silver badges20 bronze badges

asked Jun 30, 2018 at 7:57

Aaron Ullal's user avatar

Let’s assume your business logic throws an EntityNotFoundError and you want to map it to a NotFoundException.

For that, you can create an Interceptor that transforms your errors:

@Injectable()
export class NotFoundInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // next.handle() is an Observable of the controller's result value
    return next.handle()
      .pipe(catchError(error => {
        if (error instanceof EntityNotFoundError) {
          throw new NotFoundException(error.message);
        } else {
          throw error;
        }
      }));
  }
}

You can then use it by adding @UseInterceptors(NotFoundInterceptor) to your controller’s class or methods; or even as a global interceptor for all routes. Of course, you can also map multiple errors in one interceptor.

Try it out in this codesandbox.

answered Mar 19, 2019 at 10:11

Kim Kern's user avatar

Kim KernKim Kern

50.5k17 gold badges189 silver badges189 bronze badges

2

Nest Js provides an exception filter that handles error not handled in the application layer, so i have modified it to return 500, internal server error for exceptions that are not Http. Then logging the exception to the server, then you can know what’s wrong and fix it.

import 'dotenv/config';
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';

@Catch()
export class HttpErrorFilter implements ExceptionFilter {
  private readonly logger : Logger 
  constructor(){
    this.logger = new Logger 
  }
  catch(exception: Error, host: ArgumentsHost): any {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
    const response = ctx.getResponse();

    const statusCode = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
    const message = exception instanceof HttpException ?  exception.message || exception.message?.error: 'Internal server error'

    const devErrorResponse: any = {
      statusCode,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      errorName: exception?.name,
      message: exception?.message
    };

    const prodErrorResponse: any = {
      statusCode,
      message
    };
    this.logger.log( `request method: ${request.method} request url${request.url}`, JSON.stringify(devErrorResponse));
    response.status(statusCode).json( process.env.NODE_ENV === 'development'? devErrorResponse: prodErrorResponse);
  }
}

answered Sep 5, 2021 at 15:57

Ukpa Uchechi's user avatar

5

You may want to bind services not only to HTTP interface, but also for GraphQL or any other interface. So it is better to cast business-logic level exceptions from services to Http-level exceptions (BadRequestException, ForbiddenException) in controllers.

In the simpliest way it could look like

import { BadRequestException, Injectable } from '@nestjs/common';

@Injectable()
export class HttpHelperService {
  async transformExceptions(action: Promise<any>): Promise<any> {
    try {
      return await action;
    } catch (error) {
      if (error.name === 'QueryFailedError') {
        if (/^duplicate key value violates unique constraint/.test(error.message)) {
          throw new BadRequestException(error.detail);
        } else if (/violates foreign key constraint/.test(error.message)) {
          throw new BadRequestException(error.detail);
        } else {
          throw error;
        }
      } else {
        throw error;
      }
    }
  }
}

and then

answered Jun 30, 2018 at 13:44

Alexey Petushkov's user avatar

2

You could also use a factory or handler to when controller catch the exception (error or domain error) its map it to another HttpException.

@Controller('example')
export class ExampleController {

  @Post('make')
  async make(@Res() res, @Body() data: dataDTO): Promise<any> {
   
    try {
      //process result...
       return res.status(HttpStatus.OK).json(result);
    } catch (error) {
      throw AppErrorHandler.createHttpException(error); //<---here is the error type mapping
    };
  };

};

answered Nov 3, 2022 at 21:20

Dario Palminio's user avatar

Exception filters

Nest comes with a built-in exceptions layer which is responsible for processing all unhandled exceptions across an application. When an exception is not handled by your application code, it is caught by this layer, which then automatically sends an appropriate user-friendly response.

Out of the box, this action is performed by a built-in global exception filter, which handles exceptions of type HttpException (and subclasses of it). When an exception is unrecognized (is neither HttpException nor a class that inherits from HttpException), the built-in exception filter generates the following default JSON response:

{
  "statusCode": 500,
  "message": "Internal server error"
}

info Hint The global exception filter partially supports the http-errors library. Basically, any thrown exception containing the statusCode and message properties will be properly populated and sent back as a response (instead of the default InternalServerErrorException for unrecognized exceptions).

Throwing standard exceptions

Nest provides a built-in HttpException class, exposed from the @nestjs/common package. For typical HTTP REST/GraphQL API based applications, it’s best practice to send standard HTTP response objects when certain error conditions occur.

For example, in the CatsController, we have a findAll() method (a GET route handler). Let’s assume that this route handler throws an exception for some reason. To demonstrate this, we’ll hard-code it as follows:

@@filename(cats.controller)
@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

info Hint We used the HttpStatus here. This is a helper enum imported from the @nestjs/common package.

When the client calls this endpoint, the response looks like this:

{
  "statusCode": 403,
  "message": "Forbidden"
}

The HttpException constructor takes two required arguments which determine the
response:

  • The response argument defines the JSON response body. It can be a string
    or an object as described below.
  • The status argument defines the HTTP status code.

By default, the JSON response body contains two properties:

  • statusCode: defaults to the HTTP status code provided in the status argument
  • message: a short description of the HTTP error based on the status

To override just the message portion of the JSON response body, supply a string
in the response argument. To override the entire JSON response body, pass an object in the response argument. Nest will serialize the object and return it as the JSON response body.

The second constructor argument — status — should be a valid HTTP status code.
Best practice is to use the HttpStatus enum imported from @nestjs/common.

There is a third constructor argument (optional) — options — that can be used to provide an error cause. This cause object is not serialized into the response object, but it can be useful for logging purposes, providing valuable information about the inner error that caused the HttpException to be thrown.

Here’s an example overriding the entire response body and providing an error cause:

@@filename(cats.controller)
@Get()
async findAll() {
  try {
    await this.service.findAll()
  } catch (error) { 
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error
    });
  }
}

Using the above, this is how the response would look:

{
  "status": 403,
  "error": "This is a custom message"
}

Custom exceptions

In many cases, you will not need to write custom exceptions, and can use the built-in Nest HTTP exception, as described in the next section. If you do need to create customized exceptions, it’s good practice to create your own exceptions hierarchy, where your custom exceptions inherit from the base HttpException class. With this approach, Nest will recognize your exceptions, and automatically take care of the error responses. Let’s implement such a custom exception:

@@filename(forbidden.exception)
export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

Since ForbiddenException extends the base HttpException, it will work seamlessly with the built-in exception handler, and therefore we can use it inside the findAll() method.

@@filename(cats.controller)
@Get()
async findAll() {
  throw new ForbiddenException();
}

Built-in HTTP exceptions

Nest provides a set of standard exceptions that inherit from the base HttpException. These are exposed from the @nestjs/common package, and represent many of the most common HTTP exceptions:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

All the built-in exceptions can also provide both an error cause and an error description using the options parameter:

throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })

Using the above, this is how the response would look:

{
  "message": "Something bad happened",
  "error": "Some error description",
  "statusCode": 400,
}

Exception filters

While the base (built-in) exception filter can automatically handle many cases for you, you may want full control over the exceptions layer. For example, you may want to add logging or use a different JSON schema based on some dynamic factors. Exception filters are designed for exactly this purpose. They let you control the exact flow of control and the content of the response sent back to the client.

Let’s create an exception filter that is responsible for catching exceptions which are an instance of the HttpException class, and implementing custom response logic for them. To do this, we’ll need to access the underlying platform Request and Response objects. We’ll access the Request object so we can pull out the original url and include that in the logging information. We’ll use the Response object to take direct control of the response that is sent, using the response.json() method.

@@filename(http-exception.filter)
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}
@@switch
import { Catch, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter {
  catch(exception, host) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

info Hint All exception filters should implement the generic ExceptionFilter<T> interface. This requires you to provide the catch(exception: T, host: ArgumentsHost) method with its indicated signature. T indicates the type of the exception.

warning Warning If you are using @nestjs/platform-fastify you can use response.send() instead of response.json(). Don’t forget to import the correct types from fastify.

The @Catch(HttpException) decorator binds the required metadata to the exception filter, telling Nest that this particular filter is looking for exceptions of type HttpException and nothing else. The @Catch() decorator may take a single parameter, or a comma-separated list. This lets you set up the filter for several types of exceptions at once.

Arguments host

Let’s look at the parameters of the catch() method. The exception parameter is the exception object currently being processed. The host parameter is an ArgumentsHost object. ArgumentsHost is a powerful utility object that we’ll examine further in the execution context chapter*. In this code sample, we use it to obtain a reference to the Request and Response objects that are being passed to the original request handler (in the controller where the exception originates). In this code sample, we’ve used some helper methods on ArgumentsHost to get the desired Request and Response objects. Learn more about ArgumentsHost here.

*The reason for this level of abstraction is that ArgumentsHost functions in all contexts (e.g., the HTTP server context we’re working with now, but also Microservices and WebSockets). In the execution context chapter we’ll see how we can access the appropriate underlying arguments for any execution context with the power of ArgumentsHost and its helper functions. This will allow us to write generic exception filters that operate across all contexts.

Binding filters

Let’s tie our new HttpExceptionFilter to the CatsController‘s create() method.

@@filename(cats.controller)
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}
@@switch
@Post()
@UseFilters(new HttpExceptionFilter())
@Bind(Body())
async create(createCatDto) {
  throw new ForbiddenException();
}

info Hint The @UseFilters() decorator is imported from the @nestjs/common package.

We have used the @UseFilters() decorator here. Similar to the @Catch() decorator, it can take a single filter instance, or a comma-separated list of filter instances. Here, we created the instance of HttpExceptionFilter in place. Alternatively, you may pass the class (instead of an instance), leaving responsibility for instantiation to the framework, and enabling dependency injection.

@@filename(cats.controller)
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}
@@switch
@Post()
@UseFilters(HttpExceptionFilter)
@Bind(Body())
async create(createCatDto) {
  throw new ForbiddenException();
}

info Hint Prefer applying filters by using classes instead of instances when possible. It reduces memory usage since Nest can easily reuse instances of the same class across your entire module.

In the example above, the HttpExceptionFilter is applied only to the single create() route handler, making it method-scoped. Exception filters can be scoped at different levels: method-scoped, controller-scoped, or global-scoped. For example, to set up a filter as controller-scoped, you would do the following:

@@filename(cats.controller)
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

This construction sets up the HttpExceptionFilter for every route handler defined inside the CatsController.

To create a global-scoped filter, you would do the following:

@@filename(main)
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

warning Warning The useGlobalFilters() method does not set up filters for gateways or hybrid applications.

Global-scoped filters are used across the whole application, for every controller and every route handler. In terms of dependency injection, global filters registered from outside of any module (with useGlobalFilters() as in the example above) cannot inject dependencies since this is done outside the context of any module. In order to solve this issue, you can register a global-scoped filter directly from any module using the following construction:

@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

info Hint When using this approach to perform dependency injection for the filter, note that regardless of the module where this construction is employed, the filter is, in fact, global. Where should this be done? Choose the module where the filter (HttpExceptionFilter in the example above) is defined. Also, useClass is not the only way of dealing with custom provider registration. Learn more here.

You can add as many filters with this technique as needed; simply add each to the providers array.

Catch everything

In order to catch every unhandled exception (regardless of the exception type), leave the @Catch() decorator’s parameter list empty, e.g., @Catch().

In the example below we have a code that is platform-agnostic because it uses the HTTP adapter to deliver the response, and doesn’t use any of the platform-specific objects (Request and Response) directly:

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    // In certain situations `httpAdapter` might not be available in the
    // constructor method, thus we should resolve it here.
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

warning Warning When combining an exception filter that catches everything with a filter that is bound to a specific type, the «Catch anything» filter should be declared first to allow the specific filter to correctly handle the bound type.

Inheritance

Typically, you’ll create fully customized exception filters crafted to fulfill your application requirements. However, there might be use-cases when you would like to simply extend the built-in default global exception filter, and override the behavior based on certain factors.

In order to delegate exception processing to the base filter, you need to extend BaseExceptionFilter and call the inherited catch() method.

@@filename(all-exceptions.filter)
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}
@@switch
import { Catch } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception, host) {
    super.catch(exception, host);
  }
}

warning Warning Method-scoped and Controller-scoped filters that extend the BaseExceptionFilter should not be instantiated with new. Instead, let the framework instantiate them automatically.

The above implementation is just a shell demonstrating the approach. Your implementation of the extended exception filter would include your tailored business logic (e.g., handling various conditions).

Global filters can extend the base filter. This can be done in either of two ways.

The first method is to inject the HttpAdapter reference when instantiating the custom global filter:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

  await app.listen(3000);
}
bootstrap();

The second method is to use the APP_FILTER token as shown here.

In this post, we will look at perform NestJS Exception Handling.

Exception handling is extremely important for any real production application. Proper exception handling can be the difference between a user friendly application and a poorly maintained application.

  1. 1 – NestJS Exception Handler
  2. 2 – Throwing Standard Exceptions
  3. 3 – Custom Response Body
  4. 4 – Custom Exception
  5. Conclusion

1 – NestJS Exception Handler

NestJS comes with a built-in exceptions layer.

This layer is responsible for processing all unhandled exceptions. In other words, if the code we write does not handle a particular exception, the exceptions layer will handle it. It will also send an appropriate response.

Internally, there is a global exception filter that takes care of exception handling. This global exception filter handles all exceptions of type HttpException as well as any subclasses that inherit from HttpException.

When a particular exception is unrecognized, the built-in exception filter generates a default JSON response. Here, unrecognized means an exception that does not inherit from the standard HttpException directly or indirectly.

{
  "statusCode": 500,
  "message": "Internal server error"
}

As you can see, this is not a very user-friendly response message. Internal Server Error could mean a dozen things and it will be tough for the consumer to figure out what went wrong. Moreover, we would often want to throw specific exceptions in case of specific situations.

NestJS exception handling provides support for the same.

2 – Throwing Standard Exceptions

As we saw earlier, NestJS has an in-built HttpException class as part of the @nestjs/common package.

Let us see it in action.

@Get("/exceptions")
generateException() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)
}

If you see above, we create an end-point /exceptions. Here, we hard code the exception by throwing HttpStatus Forbidden. If you wish to know more about how to create controllers, please refer to this post on NestJS Controllers.

On hitting the above endpoint, we get the below response.

{
  "statusCode": 403,
  "message": "Forbidden"
}

Here, status code is 403 for Forbidden. The message is what we passed as first argument to the exception constructor.

The HttpException constructor takes 2 arguments.

  • First is the response argument. Basically, this defines the JSON response body. It can be a string or even an object.
  • The status argument defines the HTTP status code

3 – Custom Response Body

The JSON response body consists of two parts:

  • The status code that defaults to the status code provided in the constructor.
  • Second one is the message part that provides a brief description of the exception.

We can also override this standard response by providing our own response object. First approach is to provide our own string message in the response argument.

However, to override the entire JSON response body, we can pass an object in the response argument. Nest will automatically serialize the object and return it as response. See below example:

@Get("/custom-exception-object")
generateCustomExceptionObject() {
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom exception message',
    }, HttpStatus.FORBIDDEN);
}

In this case, the response will look as below:

{
  "status": 403,
  "error": "This is a custom exception message"
}

4 – Custom Exception

Mostly, we do not need to write custom exceptions. Basically, we can use the built-in HttpException.

However, if you need to write your own exception, it is better to create an exception hierarchy. In other words, the custom exception should inherit from the standard HttpException class. By doing so, NestJS will automatically recognize our custom exceptions as well and build appropriate response objects.

Let’s see a custom exception.

import { HttpException, HttpStatus } from "@nestjs/common";

export class ForbiddenException extends HttpException {
    constructor() {
        super('Custom Forbidden', HttpStatus.FORBIDDEN)
    }
}

As you can see, the ForbiddenException extends the base HttpException. Therefore, it will work seamlessly with the built-in exception handler.

See below example:

@Get("/custom-exception")
generateCustomException() {
    throw new ForbiddenException();
}

The ForbiddenException here is from the class we created and not from the standard exception library.

Conclusion

With this, we have successfully looked NestJS Exception Handling. We also looked at examples involving built-in HttpException and customizing response body. Lastly, we also learnt how to create our own custom exception.

In the follow-up post, we will also be looking at NestJS Exception Filters that will help us get even greater control over exceptions in NestJS.

If you have any comments or queries, please feel free to write in the comments section below.

  • 1. API with NestJS #1. Controllers, routing and the module structure
  • 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
  • 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
  • 4. API with NestJS #4. Error handling and data validation

NestJS shines when it comes to handling errors and validating data. A lot of that is thanks to using decorators. In this article, we go through features that NestJS provides us with, such as Exception filters and Validation pipes.

The code from this series results in this repository . It aims to be an extended version of the official Nest framework TypeScript starter .

Exception filters

Nest has an exception filter that takes care of handling the errors in our application. Whenever we don’t handle an exception ourselves, the exception filter does it for us. It processes the exception and sends it in the response in a user-friendly format.

The default exception filter is called  BaseExceptionFilter . We can look into the source code of NestJS and inspect its behavior.

nest/packages/core/exceptions/base-exception-filter.ts

export class BaseExceptionFilter<T = any> implements ExceptionFilter<T> {
  // ...
  catch(exception: T, host: ArgumentsHost) {
    // ...
    if (!(exception instanceof HttpException)) {
      return this.handleUnknownError(exception, host, applicationRef);
    }
    const res = exception.getResponse();
    const message = isObject(res)
      ? res
      : {
          statusCode: exception.getStatus(),
          message: res,
        };
    // ...
  }
 
  public handleUnknownError(
    exception: T,
    host: ArgumentsHost,
    applicationRef: AbstractHttpAdapter | HttpServer,
  ) {
    const body = {
      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
      message: MESSAGES.UNKNOWN_EXCEPTION_MESSAGE,
    };
    // ...
  }
}

Every time there is an error in our application, the catch method runs. There are a few essential things we can get from the above code.

HttpException

Nest expects us to use the HttpException class. If we don’t, it interprets the error as unintentional and responds with 500 Internal Server Error .

We’ve used HttpException quite a bit in the previous parts of this series:

throw new HttpException('Post not found', HttpStatus.NOT_FOUND);

The constructor takes two required arguments: the response body, and the status code. For the latter, we can use the provided HttpStatus enum.

If we provide a string as the definition of the response, NestJS serialized it into an object containing two properties:

  • statusCode : contains the HTTP code that we’ve chosen
  • message : the description that we’ve provided

We can override the above behavior by providing an object as the first argument of the HttpException constructor.

We can often find ourselves throwing similar exceptions more than once. To avoid code duplication, we can create custom exceptions. To do so, we need to extend the HttpException class.

posts/exception/postNotFund.exception.ts

import { HttpException, HttpStatus } from '@nestjs/common';
 
class PostNotFoundException extends HttpException {
  constructor(postId: number) {
    super(`Post with id ${postId} not found`, HttpStatus.NOT_FOUND);
  }
}

Our custom PostNotFoundException calls the constructor of the   HttpException . Therefore, we can clean up our code by not having to define the message every time we want to throw an error.

NestJS has a set of exceptions that extend the HttpException . One of them is  NotFoundException . We can refactor the above code and use it.

We can find the full list of built-in HTTP exceptions in the documentation.

posts/exception/postNotFund.exception.ts

import { NotFoundException } from '@nestjs/common';
 
class PostNotFoundException extends NotFoundException {
  constructor(postId: number) {
    super(`Post with id ${postId} not found`);
  }
}

The first argument of the NotFoundException class is an additional  error property. This way, our  message is defined by  NotFoundException and is based on the status.

Extending the BaseExceptionFilter

The default BaseExceptionFilter can handle most of the regular cases. However, we might want to modify it in some way. The easiest way to do so is to create a filter that extends it.

utils/exceptionsLogger.filter.ts

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
 
@Catch()
export class ExceptionsLoggerFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    console.log('Exception thrown', exception);
    super.catch(exception, host);
  }
}

The @ Catch ( ) decorator means that we want our filter to catch all exceptions. We can provide it with a single exception type or a list.

The ArgumentsHost hives us access to the execution context  of the application . We explore it in the upcoming parts of this series.

We can use our new filter in three ways. The first one is to use it globally in all our routes through app . useGlobalFilters .

main.ts

import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import { ExceptionsLoggerFilter } from './utils/exceptionsLogger.filter';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
 
  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new ExceptionsLoggerFilter(httpAdapter));
 
  app.use(cookieParser());
  await app.listen(3000);
}
bootstrap();

A better way to inject our filter globally is to add it to our AppModule . Thanks to that, we could inject additional dependencies into our filter.

import { Module } from '@nestjs/common';
import { ExceptionsLoggerFilter } from './utils/exceptionsLogger.filter';
import { APP_FILTER } from '@nestjs/core';
 
@Module({
  // ...
  providers: [
    {
      provide: APP_FILTER,
      useClass: ExceptionsLoggerFilter,
    },
  ],
})
export class AppModule {}

The third way to bind filters is to attach the @ UseFilters decorator. We can provide it with a single filter, or a list of them.

@Get(':id')
@UseFilters(ExceptionsLoggerFilter)
getPostById(@Param('id') id: string) {
  return this.postsService.getPostById(Number(id));
}

The above is not the best approach to logging exceptions. NestJS has a built-in Logger that we cover in the upcoming parts of this series.

Implementing the ExceptionFilter interface

If we need a fully customized behavior for errors, we can build our filter from scratch. It needs to implement the ExceptionFilter interface. Let’s look into an example:

import { ExceptionFilter, Catch, ArgumentsHost, NotFoundException } from '@nestjs/common';
import { Request, Response } from 'express';
 
@Catch(NotFoundException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: NotFoundException, host: ArgumentsHost) {
    const context = host.switchToHttp();
    const response = context.getResponse<Response>();
    const request = context.getRequest<Request>();
    const status = exception.getStatus();
    const message = exception.getMessage();
 
    response
      .status(status)
      .json({
        message,
        statusCode: status,
        time: new Date().toISOString(),
      });
  }
}

There are a few notable things above. Since we use @ Catch ( NotFoundException ) , this filter runs only for  NotFoundException .

The host . switchToHttp method returns the  HttpArgumentsHost object with information about the HTTP context. We explore it a lot in the upcoming parts of this series when discussing the execution context .

Validation

We definitely should validate the upcoming data. In theTypeScript Express series, we use the class-validator library . NestJS also incorporates it.

NestJS comes with a set of built-in pipes . Pipes are usually used to either transform the input data or validate it. Today we only use the predefined pipes, but in the upcoming parts of this series, we might look into creating custom ones.

To start validating data, we need the ValidationPipe .

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import { ValidationPipe } from '@nestjs/common';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.use(cookieParser());
  await app.listen(3000);
}
bootstrap();

In the f irst part of this series , we’ve created Data Transfer Objects. They define the format of the data sent in a request. They are a perfect place to attach validation.

npm install class-validator class-transformer

For the ValidationPipe to work we also need the class-transformer library

auth/dto/register.dto.ts

import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator';
 
export class RegisterDto {
  @IsEmail()
  email: string;
 
  @IsString()
  @IsNotEmpty()
  name: string;
 
  @IsString()
  @IsNotEmpty()
  @MinLength(7)
  password: string;
}
 
export default RegisterDto;

Thanks to the fact that we use the above RegisterDto with the  @ Body ( ) decorator, the ValidationPipe now checks the data.

@Post('register')
async register(@Body() registrationData: RegisterDto) {
  return this.authenticationService.register(registrationData);
}

There are a lot more decorators that we can use. For a full list, check out the class-validator documentation . You can also create custom validation decorators .

Validating params

We can also use the class-validator library to validate params.

utils/findOneParams.ts

import { IsNumberString } from 'class-validator';
 
class FindOneParams {
  @IsNumberString()
  id: string;
}
@Get(':id')
getPostById(@Param() { id }: FindOneParams) {
  return this.postsService.getPostById(Number(id));
}

Please note that we don’t use @ Param ( ‘id’ ) anymore here. Instead, we destructure the whole params object.

If you use MongoDB instead of Postgres, the @ IsMongoId ( ) decorator might prove to be useful for you here

Handling PATCH

In the TypeScript Express series , we discuss the difference between the PUT and PATCH methods. Summing it up, PUT replaces an entity, while PATCH applies a partial modification. When performing partial changes, we need to skip missing properties.

The most straightforward way to handle PATCH is to pass skipMissingProperties to our  ValidationPipe .

app.useGlobalPipes(new ValidationPipe({ skipMissingProperties: true }))

Unfortunately, this would skip missing properties in all of our DTOs. We don’t want to do that when posting data. Instead, we could add IsOptional to all properties when updating data.

import { IsString, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
 
export class UpdatePostDto {
  @IsNumber()
  @IsOptional()
  id: number;
 
  @IsString()
  @IsNotEmpty()
  @IsOptional()
  content: string;
 
  @IsString()
  @IsNotEmpty()
  @IsOptional()
  title: string;
}

Unfortunately, the above solution is not very clean. There are some solutions provided to override the default behavior of the ValidationPipe here .

In the upcoming parts of this series we look into how we can implement PUT instead of PATCH

Summary

In this article, we’ve looked into how error handling and validation works in NestJS. Thanks to looking into how the default BaseExceptionFilter works under the hood, we now know how to handle various exceptions properly. We know also know how to change the default behavior if there is such a need. We’ve also how to use the  ValidationPipe and the class-validator library to validate incoming data.

There is still a lot to cover in the NestJS framework, so stay tuned!

Понравилась статья? Поделить с друзьями:
  • Error handling file saving did the server never start
  • Error handling discord py
  • Error handler threw an exception
  • Error handler stm32
  • Error handler stardew valley