Architect of the empire! Praetor Augustus, the supreme judge of Rome, noticed that every system needs an efficient judicial system. When something goes wrong - a missing tribute, unauthorized access, a server error - we need a way to elegantly handle these situations. It's time to learn Exception Filters - the system of courts and tribunals of NestJS!
In the Roman Empire, every type of offense had its tribunal:
Exception Filters in NestJS work identically - they catch errors and turn them into readable HTTP responses.
NestJS provides a rich hierarchy of built-in HTTP exceptions:
1import {
2 BadRequestException, // 400
3 UnauthorizedException, // 401
4 ForbiddenException, // 403
5 NotFoundException, // 404
6 ConflictException, // 409
7 InternalServerErrorException, // 500
8 HttpException, // base HTTP exception
9} from '@nestjs/common';
10
11@Controller('tributa')
12export class TributeController {
13 private tributes = [
14 { id: 1, name: 'Aurum Galliae', amount: 50000 },
15 { id: 2, name: 'Argentum Hispaniae', amount: 30000 },
16 ];
17
18 @Get(':id')
19 findTribute(@Param('id', ParseIntPipe) id: number) {
20 const tribute = this.tributes.find(t => t.id === id);
21 if (!tribute) {
22 // Throw 404 exception
23 throw new NotFoundException(
24 `Tributum ${id} non inventum! (Tribute not found)`
25 );
26 }
27 return tribute;
28 }
29
30 @Post()
31 @UseGuards(RolesGuard)
32 createTribute(@Body() dto: any) {
33 if (!dto.name || !dto.amount) {
34 // Throw 400 exception
35 throw new BadRequestException(
36 'Tribute must contain a name and amount!'
37 );
38 }
39
40 const exists = this.tributes.find(t => t.name === dto.name);
41 if (exists) {
42 // Throw 409 exception
43 throw new ConflictException(
44 `Tribute named "${dto.name}" already exists!`
45 );
46 }
47
48 const newTribute = { id: this.tributes.length + 1, ...dto };
49 this.tributes.push(newTribute);
50 return newTribute;
51 }
52}Every built-in exception automatically generates the appropriate HTTP code and response structure. You can also use the base
HttpException with any code:1throw new HttpException('Error message', HttpStatus.I_AM_A_TEAPOT); // 418Built-in exceptions are great, but sometimes we need custom formatting of error responses. The
@Catch() decorator serves this purpose:1// filters/roman-exception.filter.ts
2import {
3 ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
4} from '@nestjs/common';
5import { Request, Response } from 'express';
6
7@Catch(HttpException)
8export class RomanExceptionFilter implements ExceptionFilter {
9 catch(exception: HttpException, host: ArgumentsHost) {
10 const ctx = host.switchToHttp();
11 const response = ctx.getResponse<Response>();
12 const request = ctx.getRequest<Request>();
13 const status = exception.getStatus();
14
15 // Custom error response format in empire style
16 response.status(status).json({
17 statusCode: status,
18 imperium: 'Roma Aeterna',
19 error: exception.message,
20 path: request.url,
21 method: request.method,
22 timestamp: new Date().toISOString(),
23 });
24 }
25}Exception Filters can be bound at three levels:
1// 1. Method level - only this method
2@Controller('tributa')
3export class TributeController {
4 @Get(':id')
5 @UseFilters(new RomanExceptionFilter()) // Only for this method
6 findTribute(@Param('id') id: string) {
7 throw new NotFoundException('Tribute not found!');
8 }
9}
10
11// 2. Controller level - entire controller
12@Controller('legiones')
13@UseFilters(RomanExceptionFilter) // Entire controller
14export class LegionController {
15 @Get(':id')
16 findLegion(@Param('id') id: string) {
17 throw new NotFoundException('Legion not found!');
18 }
19}
20
21// 3. Global level - entire application (in main.ts)
22// app.useGlobalFilters(new RomanExceptionFilter());You can create a filter that catches ALL exceptions (not just HttpException):
1// filters/all-exceptions.filter.ts
2import {
3 ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
4} from '@nestjs/common';
5
6@Catch() // Without argument = catches EVERYTHING
7export class AllExceptionsFilter implements ExceptionFilter {
8 catch(exception: unknown, host: ArgumentsHost) {
9 const ctx = host.switchToHttp();
10 const response = ctx.getResponse();
11 const request = ctx.getRequest();
12
13 const status = exception instanceof HttpException
14 ? exception.getStatus()
15 : HttpStatus.INTERNAL_SERVER_ERROR;
16
17 const message = exception instanceof HttpException
18 ? exception.message
19 : 'Internal empire server error!';
20
21 response.status(status).json({
22 statusCode: status,
23 error: message,
24 path: request.url,
25 timestamp: new Date().toISOString(),
26 });
27 }
28}It's important to understand the order - filters are executed from closest to farthest:
If the method filter handles the exception, the controller and global filters will NOT be invoked.
Exception Filters are the tribunal of your empire - they ensure that every error is handled in an elegant and understandable way for API clients. No error will go unnoticed!