Пошук уроків, статей та іншого контенту
Розберете перехоплення винятків і формування узгоджених HTTP-відповідей за допомогою exception filters.
Exception filter — це компонент NestJS, який перехоплює винятки та перетворює їх на HTTP-відповіді.
Фільтри дають змогу:
уніфікувати формат помилок;
приховати внутрішні деталі серверних помилок;
додати до відповіді час, URL або власний код помилки;
обробляти різні типи винятків окремо;
централізовано логувати помилки.
Без власного фільтра NestJS автоматично обробляє вбудовані HTTP-винятки, наприклад NotFoundException або BadRequestException. Власний фільтр потрібен, коли стандартного формату відповіді недостатньо.
NestJS має базовий клас HttpException і готові винятки на його основі:
import {
BadRequestException,
NotFoundException,
} from '@nestjs/common';
throw new BadRequestException('Некоректні дані');
throw new NotFoundException('Користувача не знайдено');Типова відповідь NestJS може мати такий вигляд:
{
"statusCode": 400,
"message": "Некоректні дані",
"error": "Bad Request"
}Виняток може містити як рядок, так і об’єкт:
throw new BadRequestException({
code: 'INVALID_EMAIL',
message: 'Некоректний формат електронної пошти',
});У такому разі HttpException зберігає передане значення у своїй відповіді.
Фільтр має:
бути позначений декоратором @Catch();
реалізувати інтерфейс ExceptionFilter;
містити метод catch().
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const context = host.switchToHttp();
const response = context.getResponse();
const request = context.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: 500;
response.status(status).json({
statusCode: status,
message: 'Сталася помилка',
path: request.url,
timestamp: new Date().toISOString(),
});
}
}@Catch()Порожній @Catch() означає, що фільтр перехоплює всі винятки.
Можна вказати конкретний тип винятку:
import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
} from '@nestjs/common';
@Catch(BadRequestException)
export class BadRequestExceptionFilter
implements ExceptionFilter
{
catch(exception: BadRequestException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
response.status(400).json({
statusCode: 400,
message: 'Запит містить некоректні дані',
});
}
}Такий фільтр працюватиме лише для BadRequestException.
ArgumentsHostМетод catch() отримує два аргументи:
catch(exception: unknown, host: ArgumentsHost) {
// ...
}exception — виняток, який було викинуто.
host містить контекст виконання. Для HTTP-запиту з нього отримують:
const context = host.switchToHttp();
const request = context.getRequest();
const response = context.getResponse();Через request можна отримати дані запиту:
request.url
request.method
request.headersЧерез response можна сформувати відповідь:
response.status(400).json({
statusCode: 400,
message: 'Помилка',
});У реальному застосунку бажано, щоб усі помилки мали однакову структуру. Наприклад:
{
"statusCode": 404,
"message": "Користувача не знайдено",
"path": "/users/42",
"timestamp": "2025-01-20T10:15:30.000Z"
}Однак HttpException.getResponse() може повернути як рядок, так і об’єкт. Тому перед формуванням відповіді потрібно обробити обидва варіанти.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const context = host.switchToHttp();
const response = context.getResponse();
const request = context.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: 500;
let message: unknown = 'Внутрішня помилка сервера';
let details: Record<string, unknown> = {};
if (exception instanceof HttpException) {
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
message = exceptionResponse;
} else if (
exceptionResponse &&
typeof exceptionResponse === 'object'
) {
const responseObject =
exceptionResponse as Record<string, unknown>;
message = responseObject.message ?? message;
details = responseObject;
}
}
response.status(status).json({
...details,
statusCode: status,
message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}Якщо виняток не є екземпляром HttpException, фільтр повертає статус 500 і загальне повідомлення. Це важливо: внутрішні деталі помилки не варто відправляти клієнту.
Фільтр можна застосувати на різних рівнях:
до одного методу контролера;
до всього контролера;
глобально до всього застосунку.
import {
Controller,
Get,
NotFoundException,
UseFilters,
} from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
@UseFilters(HttpExceptionFilter)
findOne() {
throw new NotFoundException('Користувача не знайдено');
}
}У цьому випадку фільтр діє лише для методу findOne().
Також можна передати екземпляр фільтра:
@UseFilters(new HttpExceptionFilter())Передавання класу зазвичай зручніше, оскільки NestJS може створити його через власний контейнер залежностей.
import {
Controller,
Get,
NotFoundException,
UseFilters,
} from '@nestjs/common';
@UseFilters(HttpExceptionFilter)
@Controller('users')
export class UsersController {
@Get(':id')
findOne() {
throw new NotFoundException('Користувача не знайдено');
}
@Get()
findAll() {
throw new NotFoundException('Користувачів не знайдено');
}
}Фільтр застосовуватиметься до всіх методів цього контролера.
Найчастіше єдиний формат помилок потрібен для всього HTTP API. Тоді фільтр реєструють під час запуску застосунку.
main.tsimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();Після цього фільтр перехоплюватиме винятки в усіх контролерах.
http-exception.filter.tsimport {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const context = host.switchToHttp();
const response = context.getResponse();
const request = context.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: 500;
let message: unknown = 'Внутрішня помилка сервера';
let extraFields: Record<string, unknown> = {};
if (exception instanceof HttpException) {
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
message = exceptionResponse;
} else if (
exceptionResponse &&
typeof exceptionResponse === 'object'
) {
const responseObject =
exceptionResponse as Record<string, unknown>;
message = responseObject.message ?? message;
extraFields = responseObject;
}
}
response.status(status).json({
...extraFields,
statusCode: status,
message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}app.controller.tsimport {
Controller,
Get,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
@Controller()
export class AppController {
@Get('missing')
getMissingResource() {
throw new NotFoundException('Ресурс не знайдено');
}
@Get('failure')
getServerFailure() {
throw new InternalServerErrorException();
}
}app.module.tsimport { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}main.tsimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();Запит до GET /missing поверне відповідь приблизно такого вигляду:
{
"statusCode": 404,
"message": "Ресурс не знайдено",
"error": "Not Found",
"path": "/missing",
"timestamp": "2025-01-20T10:15:30.000Z"
}Запит до GET /failure поверне:
{
"statusCode": 500,
"message": "Internal server error",
"error": "Internal Server Error",
"path": "/failure",
"timestamp": "2025-01-20T10:15:30.000Z"
}APP_FILTERЯкщо фільтр має залежності, його краще зареєструвати як провайдер через токен APP_FILTER.
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AppController } from './app.controller';
import { HttpExceptionFilter } from './http-exception.filter';
@Module({
controllers: [AppController],
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}Такий фільтр також буде глобальним, але NestJS зможе повноцінно керувати його залежностями.
Наприклад, це важливо, якщо фільтр отримує сервіс для логування:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { LoggerService } from './logger.service';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: LoggerService) {}
catch(exception: unknown, host: ArgumentsHost) {
this.logger.error(exception);
const context = host.switchToHttp();
const response = context.getResponse();
const status =
exception instanceof HttpException
? exception.getStatus()
: 500;
response.status(status).json({
statusCode: status,
message: 'Сталася помилка',
});
}
}У такому випадку потрібно використовувати APP_FILTER, а не створювати фільтр вручну через new.
Не всі помилки є HttpException. Наприклад:
@Get('unexpected')
getUnexpectedError() {
throw new Error('Помилка підключення до сервісу');
}Такий виняток не містить HTTP-статусу. Фільтр має перетворити його на відповідь 500 Internal Server Error.
Клієнту краще повертати загальне повідомлення:
{
"statusCode": 500,
"message": "Внутрішня помилка сервера"
}А повну інформацію про помилку можна записати в журнал на сервері. Не слід повертати клієнту exception.message, стек викликів або конфігураційні дані, оскільки це може розкрити внутрішню структуру застосунку.
Якщо фільтри зареєстровані на кількох рівнях, NestJS враховує область їхньої дії:
метод контролера;
контролер;
глобальний рівень.
Спеціалізований фільтр можна використати для окремого випадку, а глобальний — як загальний резервний обробник.
Наприклад:
@UseFilters(BadRequestExceptionFilter)
@Post()
createUser() {
// ...
}Глобальний фільтр при цьому продовжить обробляти інші винятки.
Неправильний фільтр:
@Catch()
export class BadFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
response.status(400).json({
statusCode: 400,
message: 'Некоректний запит',
});
}
}Такий код перетворює навіть помилки сервера на статус 400. Потрібно відрізняти HttpException від невідомих помилок.
Не варто безпосередньо повертати виняток:
response.status(500).json(exception);Об’єкт помилки може містити стек викликів, SQL-запит або конфіденційні дані. Формуйте безпечну відповідь вручну.
Якщо виняток містить об’єкт:
throw new BadRequestException({
code: 'INVALID_INPUT',
message: 'Некоректне значення',
});Фільтр, який повертає лише message, втратить поле code. Якщо API використовує додаткові поля, фільтр має зберігати їх у відповіді.
Цей варіант не дає NestJS змоги впровадити залежності:
app.useGlobalFilters(new HttpExceptionFilter());Якщо фільтр використовує сервіси, зареєструйте його через APP_FILTER.
HttpExceptionФільтр з @Catch(HttpException) не перехопить звичайний Error. Якщо потрібно мати єдиний резервний формат для всіх помилок, використовуйте @Catch() і окремо обробляйте невідомі винятки.
Exception filter перехоплює винятки та формує HTTP-відповідь.
Для створення фільтра використовують @Catch() і ExceptionFilter.
ArgumentsHost дає доступ до HTTP-запиту та відповіді.
HttpException містить статус і дані відповіді через getStatus() та getResponse().
Фільтр може діяти на рівні методу, контролера або всього застосунку.
Глобальний фільтр можна підключити через app.useGlobalFilters() або APP_FILTER.
Невідомі винятки слід перетворювати на безпечну відповідь зі статусом 500.
Єдиний формат помилок спрощує роботу клієнтських застосунків і підтримку API.