Skip to content

NestJS Day 4: What are Exception Filters?

NestJS Day 4

Abstract

This is a concise, summarized approach to learn NestJS. For more in-depth knowledge about NestJS, visit the official NestJS documentation.

Github repo for day 4

HAR file

Exception filters in NestJS are powerful components that handle all unhandled exceptions across your application. They provide a centralized way to catch and process errors, format responses, and implement custom error handling logic.

Table of Contents

  1. What are Exception Filters?
  2. Built-in Exception Handling
  3. Built-in HTTP Exceptions
  4. Custom Exception Filters
  5. Global Exception Filters
  6. Method-scoped and Controller-scoped Filters
  7. Catch-all Exception Filters
  8. Extending Base Exception Filter
  9. Best Practices
  10. Real-world Examples

1. What are Exception Filters?

Exception filters are responsible for processing all unhandled exceptions in your NestJS application. When an exception is thrown and not caught by your application code, it’s automatically handled by the exception filter layer.

Key Benefits:

  • Centralized error handling: All exceptions are processed in one place
  • Consistent response format: Standardize error responses across your API
  • Custom logic: Add logging, monitoring, or custom response formatting
  • Different scopes: Apply filters globally, per controller, or per method

2. Built-in Exception Handling

NestJS comes with a built-in global exception filter that automatically handles exceptions of type HttpException and its subclasses.

Default Response Format

When an unrecognized exception occurs (not an HttpException), the default response is:

{
"statusCode": 500,
"message": "Internal server error"
}

Throwing Standard Exceptions

The HttpException class is the base for all HTTP exceptions in NestJS:

// Basic HttpException
throw new HttpException("Forbidden", HttpStatus.FORBIDDEN);
// Custom response object
throw new HttpException(
{
status: HttpStatus.FORBIDDEN,
error: "This is a custom message",
},
HttpStatus.FORBIDDEN
);
// With error cause for logging
throw new HttpException("Forbidden access!", HttpStatus.FORBIDDEN, {
cause: new Error("User is not authorized"),
description: "User does not have permission",
});

3. Built-in HTTP Exceptions

NestJS provides a comprehensive set of built-in HTTP exceptions:

Common HTTP Exceptions

import {
BadRequestException,
UnauthorizedException,
NotFoundException,
ForbiddenException,
NotAcceptableException,
RequestTimeoutException,
ConflictException,
InternalServerErrorException,
} from "@nestjs/common";
// Basic usage
throw new BadRequestException();
throw new ForbiddenException();
throw new NotFoundException();
// With custom messages
throw new BadRequestException("Invalid input data");
throw new ForbiddenException("Access denied");
// With options
throw new BadRequestException("Something bad happened", {
cause: new Error(),
description: "Some error description",
});

Complete List of Built-in Exceptions

BadRequestException (400)

  • Invalid input validation: Missing required fields, wrong data types, or malformed JSON
  • Invalid query parameters: Negative page numbers, invalid date formats, or unsupported filters
  • Business rule violations: Age restrictions, invalid combinations of parameters
  • Malformed requests: Invalid email formats, phone numbers, or URLs
if (!email || !isValidEmail(email)) {
throw new BadRequestException("Valid email is required");
}

UnauthorizedException (401)

  • Missing authentication: No JWT token provided in request headers
  • Invalid credentials: Wrong username/password combination during login
  • Expired tokens: JWT token has expired and needs refresh
  • Invalid API keys: Malformed or non-existent API keys
if (!token || !this.jwtService.verify(token)) {
throw new UnauthorizedException("Invalid or missing authentication token");
}

NotFoundException (404)

  • Resource not found: User, product, or entity doesn’t exist by given ID
  • Endpoint not found: Accessing non-existent API routes
  • File not found: Requested document or image doesn’t exist
  • Empty search results: No matching records for search criteria
const user = await this.userService.findById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}

ForbiddenException (403)

  • Insufficient permissions: User lacks required role or permission
  • Resource ownership: Accessing another user’s private data
  • Account restrictions: Suspended or banned user accounts
  • Feature access: Premium features for free tier users
if (user.role !== "admin" && resource.ownerId !== user.id) {
throw new ForbiddenException("Access denied to this resource");
}

NotAcceptableException (406)

  • Unsupported response format: Client requests XML but only JSON available
  • Unsupported language: Requested localization not supported
  • Version incompatibility: API version not supported
  • Content negotiation failure: No acceptable content type match
if (!acceptHeader.includes("application/json")) {
throw new NotAcceptableException("Only JSON format is supported");
}

RequestTimeoutException (408)

  • Long-running operations: Database queries exceeding timeout limits
  • External API delays: Third-party service taking too long to respond
  • File processing timeout: Large file uploads or processing operations
  • Background job timeout: Scheduled tasks exceeding time limits
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new RequestTimeoutException("Operation timed out")),
30000
)
);

ConflictException (409)

  • Duplicate resources: Email already registered, username taken
  • Concurrent modifications: Version conflicts in optimistic locking
  • State conflicts: Trying to delete a resource that’s in use
  • Business rule conflicts: Overlapping appointments or bookings
const existingUser = await this.userService.findByEmail(email);
if (existingUser) {
throw new ConflictException("Email already registered");
}

GoneException (410)

  • Deprecated API endpoints: Old API versions no longer available
  • Deleted resources: Previously existing resource has been permanently removed
  • Expired content: Time-limited offers or content that’s no longer valid
  • Migrated resources: Data moved to new location permanently
if (apiVersion < MINIMUM_SUPPORTED_VERSION) {
throw new GoneException("This API version is no longer supported");
}

HttpVersionNotSupportedException (505)

  • Outdated HTTP protocol: Client using HTTP/1.0 when HTTP/1.1+ required
  • Protocol mismatch: Expecting HTTP/2 but receiving HTTP/1.1
  • Legacy client compatibility: Old clients not supporting required HTTP features
  • Security requirements: Enforcing newer HTTP versions for security
if (request.httpVersion < "1.1") {
throw new HttpVersionNotSupportedException("HTTP/1.1 or higher required");
}

PayloadTooLargeException (413)

  • File upload limits: Images, documents, or videos exceeding size limits
  • Request body size: JSON payloads too large for processing
  • Bulk operations: Too many items in batch requests
  • Memory constraints: Preventing server overload from large requests
if (file.size > MAX_FILE_SIZE) {
throw new PayloadTooLargeException("File size exceeds 10MB limit");
}

UnsupportedMediaTypeException (415)

  • Wrong file formats: Uploading unsupported file types (e.g., .exe instead of .jpg)
  • Content-Type mismatch: Sending XML with JSON Content-Type header
  • API format restrictions: Endpoint only accepts specific formats
  • Security restrictions: Blocking potentially dangerous file types
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
throw new UnsupportedMediaTypeException(
"Only JPG, PNG, and PDF files allowed"
);
}

UnprocessableEntityException (422)

  • Validation failures: Data format correct but business rules violated
  • Semantic errors: Valid JSON but invalid business logic
  • Dependency failures: Referenced entities don’t exist or are invalid
  • Complex validation: Multi-field validation failures
if (startDate > endDate) {
throw new UnprocessableEntityException("Start date must be before end date");
}

InternalServerErrorException (500)

  • Database connection failures: Unable to connect to database
  • Unexpected exceptions: Unhandled errors in application logic
  • Third-party service failures: External API returning errors
  • Configuration errors: Missing environment variables or settings
try {
await this.databaseService.connect();
} catch (error) {
throw new InternalServerErrorException("Database connection failed");
}

NotImplementedException (501)

  • Feature under development: Endpoints planned but not yet implemented
  • Platform-specific features: Features only available on certain platforms
  • Conditional functionality: Features available only under certain conditions
  • Placeholder endpoints: API design completed but implementation pending
if (feature === "advanced_analytics") {
throw new NotImplementedException("Advanced analytics coming soon");
}

ImATeapotException (418)

  • Easter eggs: Fun responses for special occasions or testing
  • API humor: Light-hearted responses to unusual requests
  • Coffee/tea jokes: Playful responses to beverage-related endpoints
  • Testing purposes: Unique status code for testing HTTP client handling
if (request.body.beverage === "coffee" && this.isTeapot) {
throw new ImATeapotException("I'm a teapot, I can't brew coffee!");
}

MethodNotAllowedException (405)

  • HTTP method restrictions: GET request to POST-only endpoint
  • Resource state limitations: DELETE on read-only resources
  • Authentication level restrictions: Methods requiring higher auth levels
  • Maintenance mode: Temporarily disabling write operations
if (request.method === "DELETE" && resource.isReadOnly) {
throw new MethodNotAllowedException(
"DELETE not allowed on read-only resources"
);
}

BadGatewayException (502)

  • Upstream service failures: Payment gateway or email service errors
  • Proxy errors: Issues with load balancers or reverse proxies
  • Microservice communication: Service-to-service communication failures
  • External API integration: Third-party APIs returning invalid responses
try {
const response = await this.paymentGateway.processPayment(data);
} catch (error) {
throw new BadGatewayException("Payment gateway is currently unavailable");
}

ServiceUnavailableException (503)

  • Maintenance mode: Scheduled maintenance or deployments
  • System overload: Server at capacity, temporarily rejecting requests
  • Database maintenance: Database backup or migration in progress
  • Circuit breaker pattern: Preventing cascade failures by temporarily blocking requests
if (this.isMaintenanceMode) {
throw new ServiceUnavailableException(
"Service temporarily unavailable for maintenance"
);
}

GatewayTimeoutException (504)

  • Upstream timeouts: External services taking too long to respond
  • Network issues: Connectivity problems with dependencies
  • Load balancer timeouts: Proxy timeouts waiting for backend services
  • Chain service failures: Multiple service calls where one times out
try {
const result = await Promise.race([
this.externalService.getData(),
this.timeoutPromise(30000),
]);
} catch (error) {
throw new GatewayTimeoutException("External service timeout");
}

PreconditionFailedException (412)

  • Conditional requests: If-Match header conditions not met
  • State validation: Resource state doesn’t match expected conditions
  • Version control: Entity version mismatch in optimistic locking
  • Business preconditions: Required conditions not met before operation
if (request.headers["if-match"] !== resource.etag) {
throw new PreconditionFailedException(
"Resource has been modified by another request"
);
}

Best Practices

  1. Provide meaningful messages: Include specific details about what went wrong
  2. Use appropriate status codes: Match the HTTP status to the actual problem
  3. Include error codes: Add custom error codes for client-side handling
  4. Log appropriately: Log server errors (5xx) but not client errors (4xx)
  5. Return helpful information: Guide users on how to fix the issue when possible

Custom Exception Filters

Create custom exception filters to implement specific error handling logic:

Basic HTTP Exception Filter

http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
ForbiddenException,
BadRequestException,
} from "@nestjs/common";
import { Request, Response } from "express";
@Catch(BadRequestException, ForbiddenException)
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 errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: exception.message || "An error occurred",
};
response.status(status).json(errorResponse);
}
}

Custom Exception Classes

Create your own exception classes for domain-specific errors:

forbidden.exception.ts
import { HttpException, HttpStatus } from "@nestjs/common";
export class CustomForbiddenException extends HttpException {
constructor(message?: string) {
super(message || "Custom forbidden message!", HttpStatus.FORBIDDEN);
}
}
// Usage
throw new CustomForbiddenException(
"You are not allowed to access this resource!"
);

5. Global Exception Filters

Method 1: Using useGlobalFilters()

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);
// Global filter with instance
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
app.module.ts
import { Module } from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";
import { HttpExceptionFilter } from "./filters/http-exception.filter";
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}

Advantages of Method 2:

  • Enables dependency injection
  • Better testability
  • Framework manages instantiation

6. Method-scoped and Controller-scoped Filters

Method-scoped Filter

// controller method
@Post('forbidden-access')
@UseFilters(new HttpExceptionFilter()) // Instance
// OR
@UseFilters(HttpExceptionFilter) // Class (recommended)
forbiddenAccess() {
throw new ForbiddenException();
}

Controller-scoped Filter

@Controller("exception-filters")
@UseFilters(HttpExceptionFilter)
export class ExceptionFiltersController {
// All methods in this controller use the filter
@Get("test-exception")
testException() {
throw new Error("This is a test exception");
}
@Post("forbidden-access")
forbiddenAccess() {
throw new ForbiddenException();
}
}

7. Catch-all Exception Filters

Create filters that catch ALL exceptions, not just HTTP exceptions:

catch-everything.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";
@Catch() // Empty decorator catches everything
export class CatchEverythingFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
// Get the HTTP adapter (platform-agnostic)
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
// Determine HTTP status
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
// Create response body
const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
error: exception instanceof Error ? exception.message : "Unknown error",
};
// Log the exception
console.error("Unhandled exception:", exception);
// Send response
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}

Usage with Global Registration

main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new CatchEverythingFilter(httpAdapter));
await app.listen(3000);
}

8. Extending Base Exception Filter

For cases where you want to extend the built-in behavior:

all-exceptions.filter.ts
import { Catch, ArgumentsHost } from "@nestjs/common";
import { BaseExceptionFilter } from "@nestjs/core";
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// Custom logic (e.g., logging)
console.error("An unhandled exception occurred:", exception);
// Call the base filter to handle the response
super.catch(exception, host);
}
}

Important Notes:

  • Method-scoped and Controller-scoped filters extending BaseExceptionFilter should NOT be instantiated with new
  • Let the framework instantiate them automatically
  • For global filters, you can inject HttpAdapter reference

9. Best Practices

1. Use Class-based Filter Registration

// Preferred
@UseFilters(HttpExceptionFilter)
// Avoid (unless you need specific configuration)
@UseFilters(new HttpExceptionFilter())

2. Implement Proper Logging

@Catch(HttpException)
export class LoggingExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(LoggingExceptionFilter.name);
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// Log the exception
this.logger.error(
`HTTP Exception: ${exception.message}`,
exception.stack,
`${request.method} ${request.url}`
);
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}

3. Environment-specific Error Details

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const isDevelopment = process.env.NODE_ENV === "development";
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message:
exception instanceof Error
? exception.message
: "Internal server error",
// Only include stack trace in development
...(isDevelopment && {
stack: exception instanceof Error ? exception.stack : undefined,
}),
};
response.status(status).json(errorResponse);
}
}

4. Multiple Filter Precedence

When using multiple filters, remember the order:

  1. Method-scoped filters
  2. Controller-scoped filters
  3. Global filters
// Specific filters are checked first
@Catch(BadRequestException)
export class BadRequestFilter implements ExceptionFilter {
/* ... */
}
// General filters are checked last
@Catch()
export class CatchAllFilter implements ExceptionFilter {
/* ... */
}

10. Real-world Examples

Example 1: API Rate Limiting Exception

rate-limit.exception.ts
import { HttpException, HttpStatus } from "@nestjs/common";
export class RateLimitException extends HttpException {
constructor(limit: number, windowMs: number) {
super(
{
statusCode: HttpStatus.TOO_MANY_REQUESTS,
message: "Too many requests",
error: "Rate limit exceeded",
limit,
windowMs,
retryAfter: Math.ceil(windowMs / 1000),
},
HttpStatus.TOO_MANY_REQUESTS
);
}
}
// rate-limit.filter.ts
@Catch(RateLimitException)
export class RateLimitFilter implements ExceptionFilter {
catch(exception: RateLimitException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const exceptionResponse = exception.getResponse() as any;
response
.status(exception.getStatus())
.header("X-RateLimit-Limit", exceptionResponse.limit)
.header("X-RateLimit-Remaining", 0)
.header("Retry-After", exceptionResponse.retryAfter)
.json(exceptionResponse);
}
}

Example 2: Database Exception Handler

database.filter.ts
import { Catch, ArgumentsHost, HttpStatus } from "@nestjs/common";
import { BaseExceptionFilter } from "@nestjs/core";
@Catch()
export class DatabaseExceptionFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// Handle specific database errors
if (
exception instanceof Error &&
exception.message.includes("duplicate key")
) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
response.status(HttpStatus.CONFLICT).json({
statusCode: HttpStatus.CONFLICT,
message: "Resource already exists",
error: "Conflict",
timestamp: new Date().toISOString(),
});
return;
}
// Handle connection errors
if (
exception instanceof Error &&
exception.message.includes("connection")
) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
response.status(HttpStatus.SERVICE_UNAVAILABLE).json({
statusCode: HttpStatus.SERVICE_UNAVAILABLE,
message: "Database temporarily unavailable",
error: "Service Unavailable",
timestamp: new Date().toISOString(),
});
return;
}
// Delegate to base filter for other exceptions
super.catch(exception, host);
}
}

Example 3: Validation Exception Filter

validation.filter.ts
import { Catch, ArgumentsHost } from "@nestjs/common";
import { ValidationError } from "class-validator";
@Catch(ValidationError)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: ValidationError[], host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const errors = exception.map((error) => ({
field: error.property,
errors: Object.values(error.constraints || {}),
}));
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
message: "Validation failed",
errors,
timestamp: new Date().toISOString(),
});
}
}

Testing Exception Filters

http-exception.filter.spec.ts
import { Test } from "@nestjs/testing";
import { HttpException, HttpStatus } from "@nestjs/common";
import { HttpExceptionFilter } from "./http-exception.filter";
describe("HttpExceptionFilter", () => {
let filter: HttpExceptionFilter;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [HttpExceptionFilter],
}).compile();
filter = module.get<HttpExceptionFilter>(HttpExceptionFilter);
});
it("should catch HttpException and format response", () => {
const mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
const mockRequest = {
url: "/test",
method: "GET",
};
const mockHost = {
switchToHttp: () => ({
getResponse: () => mockResponse,
getRequest: () => mockRequest,
}),
};
const exception = new HttpException("Test error", HttpStatus.BAD_REQUEST);
filter.catch(exception, mockHost as any);
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.json).toHaveBeenCalledWith({
statusCode: HttpStatus.BAD_REQUEST,
timestamp: expect.any(String),
path: "/test",
method: "GET",
message: "Test error",
});
});
});

Summary

Exception filters in NestJS provide a powerful and flexible way to handle errors in your application:

  1. Built-in exceptions cover most common HTTP errors
  2. Custom filters allow for specific error handling logic
  3. Multiple scopes (method, controller, global) provide flexibility
  4. Catch-all filters handle unexpected errors gracefully
  5. Inheritance from BaseExceptionFilter extends built-in functionality

Remember to:

  • Use class-based registration when possible
  • Implement proper logging and monitoring
  • Consider environment-specific error details
  • Test your exception filters thoroughly
  • Follow the principle of least privilege in error disclosure

Exception filters are essential for building robust, production-ready NestJS applications that provide consistent and informative error responses to clients while maintaining security and debugging capabilities.

Key Takeaways

  • Exception Filters: Use @Catch() decorator to handle specific or all exceptions
  • Built-in Exceptions: NestJS provides comprehensive HTTP exception classes
  • Custom Filters: Implement ExceptionFilter interface for custom error handling
  • Global Scope: Register filters globally via APP_FILTER token for dependency injection
  • Multiple Scopes: Apply filters at method, controller, or global level as needed
  • Error Responses: Standardize error response format across your application
  • Logging: Implement proper error logging for debugging and monitoring