Skip to main content

Exception Filters trong NestJS

Exception Filters là một cơ chế để bắt và xử lý exceptions (ngoại lệ) trong ứng dụng NestJS. Chúng cho phép bạn kiểm soát chính xác cách exceptions được xử lý và trả về cho client, giúp tạo ra error responses một cách consistent và professional.

Khái Niệm Exception Filter

Exception Filter là một class implement interface ExceptionFilter<T>. Nó có một method catch() được gọi khi một exception được throw ra.

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Response } from 'express';

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

response.status(500).json({
statusCode: 500,
message: 'Internal server error',
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

Built-in Exceptions

NestJS cung cấp sẵn các built-in exceptions:

import {
BadRequestException,
UnauthorizedException,
ForbiddenException,
NotFoundException,
ConflictException,
InternalServerErrorException,
NotImplementedException,
BadGatewayException,
ServiceUnavailableException,
GatewayTimeoutException,
HttpException,
} from '@nestjs/common';

// Sử dụng built-in exceptions
throw new NotFoundException('User not found');
throw new UnauthorizedException('Invalid credentials');
throw new BadRequestException('Invalid input');
throw new ConflictException('User already exists');
throw new InternalServerErrorException('Database error');

Exception Filter Basics

Cách Tạo Exception Filter Đơn Giản

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();
const exceptionResponse = exception.getResponse();

const message =
typeof exceptionResponse === 'object'
? (exceptionResponse as any).message
: exceptionResponse;

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

Sử dụng Exception Filter

Level Controller:

import { Controller, Get, UseFilters } from '@nestjs/common';

@Controller('users')
@UseFilters(HttpExceptionFilter)
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
throw new NotFoundException('User not found');
}
}

Level Method:

@Controller('users')
export class UsersController {
@Get(':id')
@UseFilters(HttpExceptionFilter)
findOne(@Param('id') id: string) {
throw new NotFoundException('User not found');
}
}

Global Level (trong main.ts):

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';

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

ArgumentsHost - Truy Cập Context

ArgumentsHost cung cấp method để truy cập request/response objects:

@Catch()
export class CustomExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
// Lấy HTTP context
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = ctx.getResponse().statusCode;

// Hoặc lấy WebSocket context
const wsCtx = host.switchToWs();
const client = wsCtx.getClient();

// Hoặc lấy RPC context
const rpcCtx = host.switchToRpc();
const data = rpcCtx.getData();
}
}

Ví Dụ Exception Filters Thực Tế

1. HTTP Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } 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();
const exceptionResponse = exception.getResponse();

const message =
typeof exceptionResponse === 'object'
? (exceptionResponse as any).message || exception.message
: exceptionResponse;

const errorResponse = {
statusCode: status,
message: Array.isArray(message) ? message : [message],
error: exception.name,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
};

response.status(status).json(errorResponse);
}
}

2. Validation Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost, BadRequestException } from '@nestjs/common';
import { Request, Response } from 'express';

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

const exceptionResponse = exception.getResponse() as any;
const validationErrors = exceptionResponse.message;

// Format lại validation errors
const formattedErrors = Array.isArray(validationErrors)
? validationErrors.reduce((acc, error) => {
acc[error.property] = error.constraints;
return acc;
}, {})
: validationErrors;

response.status(400).json({
statusCode: 400,
message: 'Validation failed',
errors: formattedErrors,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

3. Database Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Request, Response } from 'express';
import { QueryFailedError } from 'typeorm';

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

let statusCode = 500;
let message = 'Database error';

// Handle specific database errors
if (exception.driverError.code === '23505') {
// Unique constraint violation
statusCode = 409;
message = 'This record already exists';
} else if (exception.driverError.code === '23503') {
// Foreign key constraint violation
statusCode = 400;
message = 'Invalid reference to related record';
} else if (exception.driverError.code === '23502') {
// Not null constraint violation
statusCode = 400;
message = 'Required field is missing';
}

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

4. All Exceptions Filter (Catch-All)

import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

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

let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let details: any = null;

if (exception instanceof HttpException) {
statusCode = exception.getStatus();
const exceptionResponse = exception.getResponse();
message =
typeof exceptionResponse === 'object'
? (exceptionResponse as any).message || exception.message
: exceptionResponse;
} else if (exception instanceof Error) {
message = exception.message;
details = {
name: exception.name,
stack: process.env.NODE_ENV === 'development' ? exception.stack : undefined,
};
} else {
message = String(exception);
}

const errorResponse = {
statusCode,
message,
...(details && { details }),
timestamp: new Date().toISOString(),
path: request.url,
};

// Log error
console.error('Exception caught:', {
statusCode,
message,
path: request.url,
method: request.method,
exception,
});

response.status(statusCode).json(errorResponse);
}
}

5. Custom Business Logic Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

// Custom exception class
export class BusinessException extends Error {
constructor(
public statusCode: number = HttpStatus.BAD_REQUEST,
public message: string = 'Business logic error',
public code?: string,
) {
super(message);
}
}

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

response.status(exception.statusCode).json({
statusCode: exception.statusCode,
code: exception.code || 'BUSINESS_ERROR',
message: exception.message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

// Sử dụng
throw new BusinessException(
HttpStatus.CONFLICT,
'User already exists',
'USER_ALREADY_EXISTS',
);

6. Rate Limit Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

export class RateLimitException extends Error {
constructor(
public retryAfter: number,
public message: string = 'Too many requests',
) {
super(message);
}
}

@Catch(RateLimitException)
export class RateLimitExceptionFilter implements ExceptionFilter {
catch(exception: RateLimitException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();

response
.status(HttpStatus.TOO_MANY_REQUESTS)
.set('Retry-After', exception.retryAfter.toString())
.json({
statusCode: HttpStatus.TOO_MANY_REQUESTS,
message: exception.message,
retryAfter: exception.retryAfter,
});
}
}

7. Authentication Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost, UnauthorizedException } from '@nestjs/common';
import { Request, Response } from 'express';

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

const exceptionResponse = exception.getResponse() as any;

response.status(401).json({
statusCode: 401,
message: exceptionResponse.message || 'Unauthorized',
error: 'Unauthorized',
timestamp: new Date().toISOString(),
path: request.url,
// Có thể redirect tới login page hoặc API endpoint
redirect: '/auth/login',
});
}
}

8. Logging Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class LoggingExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');

catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

const statusCode =
exception instanceof HttpException
? exception.getStatus()
: 500;

const errorLog = {
timestamp: new Date().toISOString(),
method: request.method,
path: request.path,
statusCode,
message: exception.message,
userAgent: request.get('user-agent'),
ip: request.ip,
};

// Log theo mức độ
if (statusCode >= 500) {
this.logger.error('Server error', JSON.stringify(errorLog));
if (exception.stack) {
this.logger.error('Stack trace', exception.stack);
}
} else if (statusCode >= 400) {
this.logger.warn('Client error', JSON.stringify(errorLog));
}

response.status(statusCode).json({
statusCode,
message: exception.message || 'Internal server error',
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

Registering Exception Filters

Method-level

@Controller('users')
export class UsersController {
@Get(':id')
@UseFilters(HttpExceptionFilter)
findOne(@Param('id') id: string) {
throw new NotFoundException('User not found');
}
}

Class-level

@Controller('users')
@UseFilters(HttpExceptionFilter, ValidationExceptionFilter)
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
throw new NotFoundException('User not found');
}
}

Module-level (Provider)

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { UsersController } from './users.controller';
import { HttpExceptionFilter } from './filters/http-exception.filter';

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

Global-level

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './filters/all-exceptions.filter';

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

Custom Exception Classes

import { HttpStatus } from '@nestjs/common';

export class CustomException extends Error {
constructor(
public readonly statusCode: number = HttpStatus.BAD_REQUEST,
public readonly message: string = 'Custom error',
public readonly code: string = 'CUSTOM_ERROR',
public readonly details?: any,
) {
super(message);
this.name = this.constructor.name;
}
}

export class UserNotFoundException extends CustomException {
constructor(userId: string) {
super(
HttpStatus.NOT_FOUND,
`User with id ${userId} not found`,
'USER_NOT_FOUND',
{ userId },
);
}
}

export class InvalidCredentialsException extends CustomException {
constructor() {
super(
HttpStatus.UNAUTHORIZED,
'Invalid username or password',
'INVALID_CREDENTIALS',
);
}
}

export class DuplicateEmailException extends CustomException {
constructor(email: string) {
super(
HttpStatus.CONFLICT,
'Email already exists',
'DUPLICATE_EMAIL',
{ email },
);
}
}

Sử dụng Custom Exceptions

@Injectable()
export class UsersService {
constructor(private db: DatabaseService) {}

async findOne(id: string) {
const user = await this.db.users.findOne(id);
if (!user) {
throw new UserNotFoundException(id);
}
return user;
}

async create(createUserDto: CreateUserDto) {
const existing = await this.db.users.findByEmail(createUserDto.email);
if (existing) {
throw new DuplicateEmailException(createUserDto.email);
}
return this.db.users.create(createUserDto);
}

async validateCredentials(email: string, password: string) {
const user = await this.db.users.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new InvalidCredentialsException();
}
return user;
}
}

Exception Filter Chain

@Module({
controllers: [AppController],
providers: [
{
provide: APP_FILTER,
useClass: LoggingExceptionFilter,
},
{
provide: APP_FILTER,
useClass: ValidationExceptionFilter,
},
{
provide: APP_FILTER,
useClass: DatabaseExceptionFilter,
},
{
provide: APP_FILTER,
useClass: AllExceptionsFilter, // Catch-all, phải cuối cùng
},
],
})
export class AppModule {}

Thứ tự xử lý: Từ trên xuống dưới, filter đầu tiên match sẽ handle exception.

Best Practices

1. Đặc Thù Hóa Exceptions

Sử dụng specific exceptions thay vì generic:

// ❌ Sai
throw new HttpException('Error', HttpStatus.BAD_REQUEST);

// ✅ Đúng
throw new NotFoundException('User not found');
throw new BadRequestException('Invalid email format');
throw new UnauthorizedException('Missing token');

2. Consistent Error Response Format

interface ErrorResponse {
statusCode: number;
message: string | string[];
error?: string;
code?: string;
details?: any;
timestamp: string;
path: string;
}

// Tạo helper function
function createErrorResponse(
statusCode: number,
message: string,
code?: string,
details?: any,
): ErrorResponse {
return {
statusCode,
message,
code,
details,
timestamp: new Date().toISOString(),
path: request.url,
};
}

3. Logging Errors

@Catch()
export class LoggingExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');

catch(exception: unknown, host: ArgumentsHost) {
this.logger.error(exception);
// ... handle exception
}
}

4. Tidak Leak Sensitive Information

// ❌ Sai - Leak database error details
throw new Error('Unique constraint failed: User.email');

// ✅ Đúng - Generic message
throw new ConflictException('Email already exists');

5. Proper Status Codes

// Mapping của HTTP status codes:
// 400 Bad Request - Invalid input
// 401 Unauthorized - Missing/invalid auth
// 403 Forbidden - Authenticated nhưng không có permission
// 404 Not Found - Resource không tồn tại
// 409 Conflict - Duplicate resource
// 500 Internal Server Error - Server error
// 503 Service Unavailable - Server overloaded

Complete Example

// filters/custom-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
import { CustomException } from '../exceptions/custom.exception';

@Catch(CustomException, HttpException, Error)
export class CustomExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');

catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let code = 'INTERNAL_SERVER_ERROR';
let details: any = null;

if (exception instanceof CustomException) {
statusCode = exception.statusCode;
message = exception.message;
code = exception.code;
details = exception.details;
} else if (exception instanceof HttpException) {
statusCode = exception.getStatus();
const exceptionResponse = exception.getResponse();
message = typeof exceptionResponse === 'object'
? (exceptionResponse as any).message
: exceptionResponse;
code = 'HTTP_EXCEPTION';
} else if (exception instanceof Error) {
message = exception.message;
code = 'UNEXPECTED_ERROR';
}

const errorResponse = {
statusCode,
code,
message,
...(details && { details }),
timestamp: new Date().toISOString(),
path: request.url,
};

// Log error
if (statusCode >= 500) {
this.logger.error(JSON.stringify(errorResponse), exception.stack);
} else {
this.logger.warn(JSON.stringify(errorResponse));
}

response.status(statusCode).json(errorResponse);
}
}

// app.module.ts
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: APP_FILTER,
useClass: CustomExceptionFilter,
},
],
})
export class AppModule {}

Kết Luận

Exception Filters là công cụ quan trọng để:

  • Xử lý exceptions một cách consistent
  • Tạo professional error responses
  • Logging và monitoring errors
  • Bảo vệ sensitive information
  • Cải thiện user experience

Sử dụng Exception Filters đúng cách giúp bạn xây dựng ứng dụng robust, maintainable, và user-friendly.