Building Robust API Error Handling with NestJS Exception Filters
Introduction
Error handling is often an afterthought in API development, but it's crucial for creating robust, production-ready applications. NestJS provides powerful exception filters that allow you to centralize error handling, create consistent response formats, and improve both developer and user experience. In this post, we'll explore how to implement comprehensive error handling using NestJS exception filters.
Understanding NestJS Exception Filters
Exception filters in NestJS are responsible for processing all unhandled exceptions across your application. They allow you to control the exact response sent to the client when an error occurs, ensuring consistency and proper error reporting.
NestJS comes with built-in exception filters, but creating custom ones gives you complete control over error responses, logging, and monitoring.
Creating a Global Exception Filter
Let's start by creating a global exception filter that handles all types of errors:
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: this.getErrorMessage(message),
...(process.env.NODE_ENV === 'development' && {
stack: exception instanceof Error ? exception.stack : null
})
};
this.logger.error(
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : exception
);
response.status(status).json(errorResponse);
}
private getErrorMessage(message: any): string | object {
if (typeof message === 'string') {
return message;
}
if (typeof message === 'object' && message.message) {
return message;
}
return 'An unexpected error occurred';
}
} Handling Validation Errors
Validation errors require special handling to provide meaningful feedback. Let's create a specific filter for validation exceptions:
import { ExceptionFilter, Catch, ArgumentsHost, BadRequestException } from '@nestjs/common';
import { ValidationError } from 'class-validator';
@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse() as any;
// Check if it's a validation error
if (exceptionResponse.message && Array.isArray(exceptionResponse.message)) {
const validationErrors = this.formatValidationErrors(exceptionResponse.message);
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
error: 'Validation Failed',
message: 'Request validation failed',
details: validationErrors
});
} else {
// Handle other bad request exceptions
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exceptionResponse.message || 'Bad Request'
});
}
}
private formatValidationErrors(errors: string[]): object {
const formattedErrors = {};
errors.forEach(error => {
// Extract field name and constraint from validation error message
const matches = error.match(/^(\w+)\s+(.+)$/);
if (matches) {
const [, field, message] = matches;
if (!formattedErrors[field]) {
formattedErrors[field] = [];
}
formattedErrors[field].push(message);
}
});
return formattedErrors;
}
}Custom Business Logic Exceptions
Create custom exceptions for specific business logic errors:
import { HttpException, HttpStatus } from '@nestjs/common';
export class UserNotFoundException extends HttpException {
constructor(userId: string) {
super(
{
statusCode: HttpStatus.NOT_FOUND,
error: 'User Not Found',
message: `User with ID ${userId} was not found`,
code: 'USER_NOT_FOUND'
},
HttpStatus.NOT_FOUND
);
}
}
export class InsufficientPermissionsException extends HttpException {
constructor(action: string) {
super(
{
statusCode: HttpStatus.FORBIDDEN,
error: 'Insufficient Permissions',
message: `You don't have permission to ${action}`,
code: 'INSUFFICIENT_PERMISSIONS'
},
HttpStatus.FORBIDDEN
);
}
}
export class ResourceAlreadyExistsException extends HttpException {
constructor(resource: string) {
super(
{
statusCode: HttpStatus.CONFLICT,
error: 'Resource Already Exists',
message: `${resource} already exists`,
code: 'RESOURCE_EXISTS'
},
HttpStatus.CONFLICT
);
}
}Registering Exception Filters
Register your exception filters globally in your main application file:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './filters/global-exception.filter';
import { ValidationExceptionFilter } from './filters/validation-exception.filter';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Apply global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
// Apply exception filters (order matters!)
app.useGlobalFilters(
new ValidationExceptionFilter(),
new GlobalExceptionFilter(),
);
await app.listen(3000);
}
bootstrap();Advanced Error Handling with Monitoring
For production applications, integrate error monitoring and alerting:
import * as Sentry from '@sentry/node';
@Catch()
export class ProductionExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(ProductionExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
// Log to Sentry for 5xx errors
if (status >= 500) {
Sentry.captureException(exception, {
extra: {
url: request.url,
method: request.method,
headers: request.headers,
body: request.body,
}
});
}
// Create user-friendly error response
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: status >= 500
? 'An internal error occurred. Our team has been notified.'
: this.getErrorMessage(exception),
requestId: request.headers['x-request-id'] || 'unknown'
};
this.logger.error(
`[${errorResponse.requestId}] ${request.method} ${request.url} - ${status}`,
exception instanceof Error ? exception.stack : exception
);
response.status(status).json(errorResponse);
}
private getErrorMessage(exception: unknown): string {
if (exception instanceof HttpException) {
const response = exception.getResponse();
if (typeof response === 'string') return response;
if (typeof response === 'object' && response['message']) {
return response['message'];
}
}
return 'An unexpected error occurred';
}
} Best Practices
- Consistent Response Format: Always return errors in the same structure across your API
- Appropriate Status Codes: Use proper HTTP status codes for different error types
- Security: Never expose sensitive information in error messages
- Logging: Log errors with sufficient context for debugging
- Monitoring: Integrate with monitoring tools for production error tracking
- User-Friendly Messages: Provide clear, actionable error messages for clients
Conclusion
Proper exception handling is essential for building reliable APIs. NestJS exception filters provide a powerful way to centralize error handling, ensuring consistent responses and better debugging capabilities. By implementing custom exception filters and following best practices, you can create APIs that are both developer-friendly and production-ready.
Related Posts
Mastering Laravel Queues: A Complete Guide to Background Job Processing
Learn how to implement and optimize Laravel queues for better application performance and user experience.
Building a Real-Time Chat Application with NestJS and WebSockets
Learn to build a production-ready real-time chat application using NestJS WebSocket gateway and Socket.IO integration.
Building Scalable GraphQL APIs with DataLoader in Node.js
Learn how to eliminate the N+1 query problem in GraphQL using Facebook's DataLoader pattern for efficient data fetching.