Authentication과 Authorization에 대한 이해가 깊어짐에 따라, NestJS에서의 예외 처리에 대한 고민이 생겼습니다. 이번 글에서는 NestJS에서 예외를 어떻게 처리하는지 정리해보겠습니다.

NestJS는 HttpException이라는 기본 예외 클래스를 제공하여 에러를 HTTP 형식으로 처리할 수 있도록 돕습니다. 해당 클래스는 @nestjs/common 패키지에 포함되어 있습니다. API 요청 중 오류가 발생하면 HttpException을 사용하여 클라이언트에게 상세한 오류 메시지를 제공할 수 있습니다.
다음은 GET 요청에 대해 예외를 던지는 예시입니다:
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
여기서 첫 번째 인자인 Forbidden은 클라이언트에게 전달될 에러 메시지이고, 두 번째 인자인 HttpStatus.FORBIDDEN은 HTTP 상태 코드를 나타냅니다.
클라이언트의 응답은 다음과 같습니다:
{
"statusCode": 403,
"message": "Forbidden"
}
에러 메시지만 변경하고 싶다면 첫 번째 인자를 다른 문자열로 변경하면 됩니다. 만약 전체 응답 본문을 변경하고 싶다면, 객체를 전달할 수도 있습니다:
@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
});
}
}
변경된 응답은 다음과 같습니다:
{
"status": 403,
"error": "This is a custom message"
}
NestJS는 기본적으로 HttpException과 같은 예외를 콘솔에 출력하지 않습니다. 이는 이러한 예외가 애플리케이션 흐름의 일부로 간주되기 때문입니다.
하지만 개발 중에 예외도 콘솔에 출력하고 싶다면, ExceptionFilter를 커스터마이징하여 로깅할 수 있습니다. 이 부분은 추후에 더 다루겠습니다.
또한, 사용자 정의 예외 클래스를 만들어서 예외를 더 명확하게 구분하고 재사용성을 높이는 것도 좋은 방법입니다.
NestJS는 기본 HttpException 클래스를 제공하지만, 프로젝트의 상황에 맞춰 의미 있는 이름을 가진 사용자 정의 예외 클래스를 만들 수 있습니다. 예를 들어, 접근이 금지된 경우를 명시적으로 처리하려면 ForbiddenException을 만들 수 있습니다:
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
이제 이 사용자 정의 예외는 다음과 같이 사용할 수 있습니다:
@Get()
async findAll() {
throw new ForbiddenException();
}

NestJS는 기본 HttpException을 상속하는 여러 내장 예외 클래스를 제공합니다. 이러한 예외 클래스들은, 자주 발생하는 HTTP 오류 상황을 다루기 위해 만들어졌습니다. 예외 처리 시 필요에 따라 적절한 클래스를 활용할 수 있습니다.
옵션 파라미터를 사용하여 오류의 원인과 설명을 추가할 수 있습니다:
throw new BadRequestException('Something bad happened', {
cause: new Error(),
description: 'Some error description',
});
응답은 다음과 같습니다:
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400
}
예외 필터는 기본 예외 처리 방식으로 처리하기 어려운 특별한 예외를 다루기 위해 사용됩니다. 예를 들어, 에러가 발생했을 때 단순히 에러 코드만 보내는 대신, 추가적인 정보를 남기거나 로그를 기록하고 싶을 때 사용할 수 있습니다.
다음은 HttpException 타입의 예외만을 처리하는 예외 필터 예시입니다:
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,
});
}
}
catch() 메서드의 매개변수인 exception과 host를 살펴보겠습니다.
exception: 현재 처리 중인 예외 객체입니다. HttpException 타입이므로, 상태 코드와 메시지를 포함한 예외 정보를 가져올 수 있습니다.
host: ArgumentsHost 객체로, NestJS에서 제공하는 객체로 다양한 실행 컨텍스트의 인수에 접근할 수 있습니다. 예외 필터에서 ArgumentsHost를 사용하면 HTTP 요청/응답 객체를 손쉽게 다룰 수 있습니다. 주요 메서드로는 switchToHttp()가 있으며, 이를 통해 HTTP 요청/응답 객체에 접근할 수 있습니다.
@UseFilters() 데코레이터를 사용하면 예외 필터를 특정 컨트롤러나 메서드에 바인딩할 수 있습니다. 예를 들어, HttpExceptionFilter를 다음과 같이 사용할 수 있습니다:
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
인스턴스 대신 클래스에 필터를 적용하는 것이 좋습니다. 이렇게 하면 필터를 전역적으로 재사용할 수 있어 메모리 효율성을 높일 수 있습니다:
@Controller()
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
전역 필터를 적용하려면 useGlobalFilters() 메서드를 사용하면 됩니다:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
전역 필터는 애플리케이션의 모든 컨트롤러와 라우트 핸들러에서 사용됩니다.
모든 예외를 처리하려면 @Catch() 데코레이터의 매개변수를 비워두면 됩니다. 이렇게 하면 HttpException 외의 예외도 처리할 수 있습니다:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export class CatchEverythingFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
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);
}
}
애플리케이션 요구사항을 충족시키기 위해 맞춤화된 예외 필터를 생성할 수 있지만, 기본 제공되는 예외 필터를 확장하여 사용하는 방법도 있습니다. BaseExceptionFilter를 상속받아 기본 필터의 동작을 확장할 수 있습니다:
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);
}
}
전역 필터로 확장하려면 HttpAdapter 참조를 주입하거나 APP_FILTER를 사용하여 필터를 설정할 수 있습니다:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
NestJS에서의 예외 처리는 단순한 오류 메시지 전달을 넘어, 응답 일관성 유지와 로깅, 디버깅 효율성까지 책임지는 중요한 설계 포인트입니다.
단순한 에러 응답은 HttpException과 내장 예외 클래스 (BadRequestException, ForbiddenException 등)로 처리할 수 있고, 보다 구체적인 상황에서는 커스텀 예외 클래스를 만들어 재사용성과 가독성을 높일 수 있습니다. 복잡한 예외 흐름을 관리하려면 Exception Filter를 도입하여, 로깅, 포맷팅, 전역 처리 등 다양한 방식으로 예외를 다룰 수 있습니다.
NestJS는 예외를 ‘흐름 제어’의 한 형태로 보고 있으며, 필터나 상속을 통해 유연하게 커스터마이징할 수 있도록 설계되어 있습니다. 따라서, 예외 처리는 단순히 try-catch에 머무르지 않고, 애플리케이션 전반의 안정성과 UX를 향상시키는 도구로 활용되어야 할 것입니다.
reference: https://docs.nestjs.com/exception-filters#binding-filters