Nest Exception Filters: Navigating Error Realms with Finesse

Nest Exception Filters: Navigating Error Realms with Finesse

Circus performers perform a variety of acts (which could be dangerous) intended to amaze, thrill, and engage audiences. In a circus, a safety net is placed below performers on a high wire or trapeze to catch them and prevent them from being injured if they fall off. If every act of the performance goes well, the safety net doesn't get used, but if there is an unexpected failure, then the net comes to the rescue.

Just as safety nets catch performers in case of unexpected falls, Nest's Exception Filters catch and process unhandled (usually unexpected) exceptions in your application, ensuring a graceful response even when errors occur.

Nest's Exception Layer and the Global Exception Filter

Nest includes a special layer (called the exception layer) that captures errors not managed by your code. This layer then sends a suitable, user-friendly response. This is done automatically by Nest's pre-built global exception filter.

Nest being customizable also makes it possible for you to have full control over the exceptions layer. You may want to do this because you want to add centralized logging or return a customized response to the client when different kinds of exceptions occur. Nest has Exception Filters for that purpose.

Catching different types of Exceptions

Exceptions can occur in different forms within an application. Some exceptions are

  • HTTP exceptions are exceptions thrown when an HTTP request fails or there is an error related to HTTP communication. Nest provides the HttpException class from @nestjs/common for creating consistent HTTP response objects, recommended for handling error conditions in REST/GraphQL APIs.

  • Query and Database exceptions occur when working with databases; they are related to queries or database interactions

  • Third-party exceptions: If your application integrates with external services (like APIs), exceptions can be thrown from the interaction.

  • Custom exceptions: As a developer, you can also define custom exceptions. For example, by extending the HttpException class from Nest or the base Error class from TypeScript.

  • General Runtime Exceptions: These exceptions cover a wide range of errors that might occur during runtime, such as type errors, null reference errors, and other unexpected issues

Exception filters are customizable, you could have filters that catch only HTTP exceptions or those that catch any kind of exception.

Let's create an exception filter that catches all types of exceptions in a file src/filters/all-exception-filter.ts :

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };
    // you can log the response before sending it to the user
    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

I shamelessly copied the code from the Nest documentation by the way :)

To further illustrate, the flexibility of Nest Exception filters, we will create an Exception Filter that is responsible for catching exceptions that are instances of the HttpException class. We will implement custom response logic for them, let's name the file http-exception-filter.ts inside the src/filters folder:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
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();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

Again, I shamelessly copied this from the Nest documentation :) What it does is to get the HTTP context, and then access the response and request from the context. It then uses information from them to customize the response sent to the user.

In the response, the request URL is included as part of the response object, this way the user can know which path is throwing the exception. This can help in error reporting and debugging.

Binding Filters

Just like binding guards, we can bind filters at different levels: method-scope of the controller, controller-scoped or global-scoped. To bind filters at the method or controller level, we use the @UseFilters from the @nestjs/common package.

Let's bind the HttpExceptionFilter to a method in the UserController. Let's bind it to the getUsers route handler:

import {
  ...
  UseFilters,
} from '@nestjs/common';
import { UserService } from './user.service';
import { HttpExceptionFilter } from '../filters/http-exception-filter';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  @UseFilters(HttpExceptionFilter)
  getUsers() {
    return this.userService.getUsers();
  }
...

To see the exception filter in action, let's force an exception. In the getUsers route handler of the UserController :

import {
  ...
  HttpException,
  HttpStatus,
  UseFilters,
} from '@nestjs/common';
import { UserService } from './user.service';
import { HttpExceptionFilter } from '../filters/http-exception-filter';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  @UseFilters(HttpExceptionFilter)
  getUsers() {
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
    return this.userService.getUsers();
  }
  ...

Testing out the endpoint in Postman:

Binding filters globally

Let's show how we can bind filters globally. Just as you can bind guards globally, you can also bind filters globally by registering them at the application level on the app instance or by registering them as a provider in any module, preferably the root module.

To register exception filters globally on the app instance, we use app.useGlobalFilters) in main.ts as follows:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AdminGuard } from './auth.guard';
import { HttpExceptionFilter } from './filters/http-exception-filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

To register an exception at the root module level instead, we update the app.module.ts as follows:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './filters/exception-filter';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}

The APP_FILTER token is a special token from @nestjs/core, it is a way to register a Guard as a global guard by registering as a custom provider with that token.

Conclusion

Nest prides itself on having a platform that provides all the tools you will need to build efficient, scalable Node.js server-side applications. Throughout this series, we walked through the core building blocks of Nest. Starting with Dependency Injection, Modules, Providers, Custom Providers, Controllers, Guards, Pipes, DTOs, Validating DTOs, and Exception Filters in Nest, it's been a long ride.

The complete project we built throughout this series is available here on Github.

There are other useful components and techniques in Nest, and with the knowledge gained from this article, you are in a good position to understand them. The Nest documentation is a good resource to check out.

Some of the next topics I will recommend are

Happy coding :)