We use cookies to enhance your experience on the site
CodeWorlds

Exception Filters - courts and tribunals of the empire

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!

What are Exception Filters?

In the Roman Empire, every type of offense had its tribunal:

  • Praetorian tribunal - civil cases (404 Not Found)
  • Censor's court - access control (403 Forbidden)
  • Military tribunal - serious violations (500 Internal Error)

Exception Filters in NestJS work identically - they catch errors and turn them into readable HTTP responses.

Built-in NestJS exceptions

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); // 418

Creating Custom Exception Filters

Built-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}

Binding filters - tribunal levels

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());

Catch-All Filter - the supreme tribunal

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}

Filter execution order

It's important to understand the order - filters are executed from closest to farthest:

  1. Method filter (if exists) - checked first
  2. Controller filter (if exists) - checked second
  3. Global filter (if exists) - checked last

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!

Go to CodeWorlds