NestJS Exception Filters & Error Handling
Catching Errors Like a Pro
Open interactive version (quiz + challenge)Real-world analogy
Exception filters are like an emergency room in a hospital. When something goes wrong with a patient (an error in your code), instead of panicking in the hallway, they are routed to the ER (filter) where trained staff handle the situation calmly, document what happened, and communicate clearly with the family (client). Without an ER, every emergency would be chaos.
What is it?
Exception filters catch errors thrown during request processing and convert them into appropriate HTTP responses. NestJS provides built-in HTTP exceptions for common status codes, a default global filter, and the ability to create custom filters for specialized error handling.
Real-world relevance
Every production API needs consistent error handling. Stripe returns structured error objects with codes, messages, and docs links. GitHub API uses consistent error formats across hundreds of endpoints. Exception filters make this level of consistency effortless.
Key points
- Built-in HTTP Exceptions — NestJS provides exceptions for every HTTP status: NotFoundException (404), BadRequestException (400), UnauthorizedException (401), ForbiddenException (403), ConflictException (409), InternalServerErrorException (500). Throw them anywhere — NestJS handles the rest.
- Default Exception Filter — NestJS has a built-in global exception filter that catches ALL unhandled errors and returns a JSON response with statusCode, message, and error. Unknown errors become 500 Internal Server Error. You get sane defaults out of the box!
- Custom Exception Filters — Create filters with @Catch(ExceptionType) to handle specific errors your way. Implement ExceptionFilter with a catch(exception, host) method. You control the response format, status code, logging, and error reporting. One filter, consistent errors everywhere.
- Catch Everything — @Catch() with no arguments catches ALL exceptions — including non-HTTP errors like TypeErrors and database errors. This is your last line of defense. Log the error, send an alert, and return a generic 'something went wrong' message to the user.
- Custom Exception Classes — Extend HttpException to create domain-specific errors: `class UserNotFoundException extends NotFoundException`. Add custom properties like error codes: `{ code: 'USER_NOT_FOUND', message: '...' }`. Makes error handling more semantic and easier to debug.
- Applying Filters — @UseFilters(MyFilter) per route/controller, or app.useGlobalFilters(new MyFilter()) globally. Global filters catch errors from any route. You can stack multiple filters — they execute in order until one handles the exception.
- Error Logging & Monitoring — In your filter, log errors to external services: Sentry, DataDog, or a custom logger. Include request details (URL, method, body, user) for debugging. Production apps need error tracking — filters are the perfect place to add it.
- Validation Errors — When ValidationPipe rejects input, it throws BadRequestException with an array of error messages. Customize this by providing an exceptionFactory to the pipe. Return field-level errors: `{ field: 'email', errors: ['must be valid email'] }` for better frontend integration.
Code example
// 1. Throwing built-in exceptions
import {
NotFoundException, BadRequestException,
ConflictException, ForbiddenException,
} from '@nestjs/common';
@Injectable()
export class UsersService {
async findOne(id: number) {
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async create(dto: CreateUserDto) {
const exists = await this.prisma.user.findUnique({
where: { email: dto.email },
});
if (exists) {
throw new ConflictException('Email already registered');
}
return this.prisma.user.create({ data: dto });
}
}
// 2. Custom Exception Filter — catch ALL HTTP errors
import {
ExceptionFilter, Catch, ArgumentsHost,
HttpException, HttpStatus, Logger,
} from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger('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 message = exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
// Log the error with context
this.logger.error(
`${request.method} ${request.url} — ${status}`,
exception instanceof Error ? exception.stack : '',
);
response.status(status).json({
success: false,
statusCode: status,
message: typeof message === 'string'
? message
: (message as any).message || message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
// 3. Custom domain exception
export class UserNotFoundException extends NotFoundException {
constructor(userId: number) {
super({
code: 'USER_NOT_FOUND',
message: `User #${userId} not found`,
userId,
});
}
}
// Usage: throw new UserNotFoundException(123);
// 4. Apply globally in main.ts
// app.useGlobalFilters(new AllExceptionsFilter());
// 5. Custom validation error format
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
exceptionFactory: (errors) => {
const formatted = errors.map(err => ({
field: err.property,
errors: Object.values(err.constraints || {}),
}));
return new BadRequestException({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
errors: formatted,
});
},
}));Line-by-line walkthrough
- 1. 1. Throwing built-in exceptions
- 2. Import NestJS exception classes
- 3.
- 4.
- 5.
- 6.
- 7. Injectable service class
- 8. Find a single user by ID
- 9. Query the database
- 10. If user doesn't exist...
- 11. Throw 404 with a descriptive message
- 12. Close if
- 13. Return the found user
- 14. Close method
- 15.
- 16. Create a new user
- 17. Check if email already exists
- 18. Query by email
- 19. Close query
- 20. If user exists with this email...
- 21. Throw 409 Conflict error
- 22. Close if
- 23. Create the user in the database
- 24. Close method
- 25. Close class
- 26.
- 27. 2. Custom Exception Filter — catch ALL errors
- 28. Import filter interfaces
- 29.
- 30.
- 31.
- 32.
- 33. @Catch() with no argument = catch everything
- 34. Implementing ExceptionFilter interface
- 35. Logger for error reporting
- 36.
- 37. catch method — exception and host
- 38. Switch to HTTP context
- 39. Get the response object
- 40. Get the request object
- 41.
- 42. Determine status code
- 43. If it's an HttpException, use its status
- 44. Otherwise, default to 500
- 45.
- 46. Determine error message
- 47. If HttpException, get its response
- 48. Otherwise, generic message
- 49.
- 50. Log the error with context
- 51. Format: GET /users — 404
- 52. Include stack trace for real errors
- 53. Close logger call
- 54.
- 55. Send the response
- 56. success: false flag
- 57. The HTTP status code
- 58. Extract the message string
- 59. Handle both string and object messages
- 60. Include the request path
- 61. Include a timestamp
- 62. Close json response
- 63. Close catch method
- 64. Close class
- 65.
- 66. 3. Custom domain exception
- 67. Extend NotFoundException for specific case
- 68. Constructor takes userId
- 69. Call parent with structured error object
- 70. Custom error code
- 71. Descriptive message
- 72. Include the userId for debugging
- 73. Close super call
- 74. Close constructor
- 75. Close class
- 76.
- 77. Usage: throw with specific user ID
- 78.
- 79. 4. Apply globally in main.ts
- 80.
- 81. 5. Custom validation error format
- 82. Configure ValidationPipe with custom factory
- 83. Standard options
- 84.
- 85. Custom error formatter
- 86. Map each validation error
- 87. Include the field name
- 88. Include all constraint messages
- 89. Close map
- 90. Return a structured BadRequestException
- 91. Custom error code
- 92. Clear message
- 93. Field-level errors
- 94. Close return
- 95. Close factory
- 96. Close pipe config
Spot the bug
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
response.json({
statusCode: status,
message: exception.message,
});
}
}Need a hint?
The response is being sent but something is missing. What should you set before sending JSON?
Show answer
The filter sends JSON but never sets the HTTP status code on the response! It will always return 200 even for 404 or 500 errors. Fix: change response.json() to response.status(status).json(). Without .status(status), the browser/client thinks everything is fine!
Explain like I'm 5
You know when you play a video game and you make a mistake? Instead of the game crashing, it shows 'Game Over' with your score. Exception filters are like that — when your code makes a mistake, instead of crashing everything, the filter catches it and shows a nice error message like 'Sorry, we couldn't find that!'
Fun fact
HTTP status codes were defined in 1999 by RFC 2616. The famous 404 Not Found was supposedly named after Room 404 at CERN, where the original web servers were located. When people couldn't find a page, they'd say 'check room 404' — and the name stuck!
Hands-on challenge
Create an AllExceptionsFilter that: 1) Logs errors with request details, 2) Returns a consistent JSON format with statusCode, message, code, path, and timestamp, 3) Handles both HttpExceptions and unknown errors, 4) Create a custom DuplicateEmailException class. Apply the filter globally and test with different error scenarios!
More resources
- NestJS Exception Filters (NestJS Official)
- NestJS Built-in HTTP Exceptions (NestJS Official)
- HTTP Status Codes Reference (MDN Web Docs)