Когда в приложении что-то идёт не так, клиент получает ответ с ошибкой. Вопрос: кто превращает внутреннее исключение в понятный HTTP-ответ, и как сделать так, чтобы этот процесс был единообразным по всему API? В NestJS за это отвечают exception filters.
Что происходит без фильтров
Представьте: где-то в коде бросается необработанное исключение. Без настройки NestJS вернёт 500 Internal Server Error, а в теле может оказаться стек-трейс — то есть внутренние детали реализации уйдут клиенту. Это плохо: клиент видит бесполезный мусор, а не понятное сообщение об ошибке.
Плюс разные части приложения могут отвечать по-разному: один маршрут возвращает { message: "not found" }, другой — { error: "NotFound" }, третий — вообще строку. Клиент не может на это нормально реагировать.
Exception filter — это класс, который перехватывает исключение и формирует из него правильный HTTP-ответ. Один фильтр можно подключить глобально — тогда все ошибки пройдут через него.
HttpException: встроенный способ вернуть ошибку
Самый простой путь — бросить HttpException или один из его готовых наследников. NestJS умеет их обрабатывать сам, без дополнительных фильтров.
import { NotFoundException } from '@nestjs/common';
@Get(':id')
async getOne(@Param('id', ParseIntPipe) id: number) {
const product = await this.repo.find(id);
if (!product) {
throw new NotFoundException(`product ${id} not found`);
}
return product;
}
NotFoundException — это HttpException с кодом 404. В NestJS есть готовые классы для самых частых случаев: BadRequestException (400), UnauthorizedException (401), ForbiddenException (403), NotFoundException (404), ConflictException (409), InternalServerErrorException (500) и другие.
Для простых случаев, когда ошибка очевидна прямо в контроллере, этого достаточно.
Свой фильтр: доменное исключение → HTTP
Проблема возникает, когда бизнес-логика находится не в контроллере, а глубже — в сервисах, обработчиках команд. Им не стоит знать о HTTP: они должны бросать исключения на языке домена, а не протокола.
Например, сервис знает, что товар не найден, и бросает ProductNotFound. Кто должен превратить это в 404? Не сервис — он про бизнес. Не контроллер — ему придётся оборачивать каждый вызов в try/catch. Для этого и нужен exception filter.
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
export class ProductNotFound extends Error {
constructor(public readonly productId: number) {
super(`product ${productId} not found`);
}
}
@Catch(ProductNotFound)
export class ProductNotFoundFilter implements ExceptionFilter {
catch(exception: ProductNotFound, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
response.status(HttpStatus.NOT_FOUND).json({
error: 'product_not_found',
productId: exception.productId,
});
}
}
@Catch(ProductNotFound) говорит NestJS: «этот фильтр перехватывает только исключения типа ProductNotFound». Внутри метода catch — формируем ответ: статус 404 и тело с понятным кодом ошибки.
Теперь сервис пишет throw new ProductNotFound(id) — чисто, без знания о HTTP. Фильтр делает перевод в протокол.
Глобальный фильтр: единый формат для всего API
Хорошая практика — сделать так, чтобы все ошибки в API выглядели одинаково. Клиент заранее знает структуру ответа об ошибке и может её обрабатывать автоматически.
Для этого создают глобальный фильтр, который ловит всё подряд (@Catch() без аргументов):
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException ? exception.message : 'internal_error';
response.status(status).json({
statusCode: status,
error: message,
});
}
}
Подключают в main.ts:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
Или через провайдер в модуле — тогда фильтр может использовать зависимости (например, логгер):
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
После этого клиент всегда получает одинаковую структуру, а необработанные ошибки не утекают стек-трейсом наружу.
Где какое исключение бросать
Простое правило, которого стоит придерживаться:
- Сервисы и бизнес-логика бросают доменные исключения (
ProductNotFound,OrderAlreadyPaid). Они описывают бизнес-ситуацию, ничего не зная о HTTP. - Контроллер может бросить
HttpExceptionнапрямую — для простых случаев, которые очевидны уже на уровне входных данных. - Exception filter переводит доменные исключения в коды HTTP и формирует единое тело ответа.
Главное: не тащите HttpException в сервисы и домен. Иначе бизнес-логика начнёт зависеть от протокола, и её станет труднее тестировать и переиспользовать.
Коротко
- Exception filter — класс с
@Catch(), который перехватывает исключение и формирует HTTP-ответ. @Catch(SomeError)ловит только конкретный тип;@Catch()без аргументов — всё подряд.HttpExceptionи его наследники (NotFoundException,BadRequestExceptionи др.) NestJS обрабатывает сам.- Сервисы бросают доменные исключения; фильтр переводит их в HTTP — это разделение ответственности.
- Глобальный фильтр подключается через
app.useGlobalFilters()или провайдерAPP_FILTER. - Единый формат ошибок — клиент всегда знает структуру ответа; необработанные исключения не утекают деталями реализации.
Что почитать дальше
- Pipes и валидация — как NestJS отсекает некорректные входные данные ещё до контроллера.
- Guards: авторизация запросов — как запрещать доступ до выполнения хендлера.
- Observability в NestJS — логирование, метрики и трассировка, включая ошибки.