Handle your exceptions by using global exception filter in a NestJS application

NestJS is a very modern and very powerful Node.js framework which offers enjoyable development experience. It is originally developed by Kamil Mysliwiec, it is heavily inspired by Angular and it rising up in popularity very quickly and for good reasons:

  • Gives a great flexibility because of the completely modular architecture and the ability of using other libraries
  • Suitable for any kind of applications, web APIs, cron jobs, small or enterprise apps
  • It uses modern JavaScript, is built with TypeScript (preserves compatibility with pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming)
  • Reach with features for fast development, out of the box

What I would like to show you in this quick article, is how to remove your manual exception handling from all of your endpoints, if you are using NestJS. This means no more try and catch blocks in your code (or you can at least reduce their usage a lot) that are actually ruing your your code readability.

In order to understand the rest of the article, you will need to go through all the fundamentals about the framework reading its documentation.

NestJS comes with a modern and handy feature called Exception Filters. What this feature is a built-in exceptions layer which is responsible for processing all unhandled exceptions across the application. When an exception is not handled by your application code, it is caught by this layer. What you can technically do and use this feature is in case you have a REST API, you can remove your try and catch blocks by just creating a simple NestJS global scoped filter by creating a simple class called all-exceptions.filter.ts (please be aware that we are going to use TypeScript for the examples, however NestJS is compatible with pure Javascript too):

import { Catch, ArgumentsHost, Inject, HttpServer, HttpStatus } from '@nestjs/common';
import { BaseExceptionFilter, HTTP_SERVER_REF } from '@nestjs/core';
import { AppLoggerService } from '../modules/shared/services/logger.service';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  constructor(
    @Inject(HTTP_SERVER_REF) applicationRef: HttpServer,
    private logger: AppLoggerService
  ) {
    super(applicationRef);
  }

  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    // const request = ctx.getRequest();
    // const status = exception.getStatus();

    this.logger.error(exception);

    let status = HttpStatus.INTERNAL_SERVER_ERROR;

    const message = (exception instanceof Error) ? exception.message : exception.message.error;

    if (exception.status === HttpStatus.NOT_FOUND) {
      status = HttpStatus.NOT_FOUND;
    }

    if (exception.status === HttpStatus.SERVICE_UNAVAILABLE) {
      status = HttpStatus.SERVICE_UNAVAILABLE;
    }

    if (exception.status === HttpStatus.NOT_ACCEPTABLE) {
      status = HttpStatus.NOT_ACCEPTABLE;
    }

    if (exception.status === HttpStatus.EXPECTATION_FAILED) {
      status = HttpStatus.EXPECTATION_FAILED;
    }

    if (exception.status === HttpStatus.BAD_REQUEST) {
      status = HttpStatus.BAD_REQUEST;
    }

    response
      .status(status)
      .json({
        status,
        success: false,
        data: [],
        error: message,
        message: (status === HttpStatus.INTERNAL_SERVER_ERROR) ? 'Sorry we are experiencing technical problems.' : '',
      });
  }
}

But, let’s go one by one.

In order to delegate exception processing to the base filter, you need to extend BaseExceptionFilter and call the inherited catch() method. Now, if you like to catch every unhandled exception (regardless of the exception type), leave the @Catch() decorator’s parameter list empty, like the example above. Now all of your exceptions will be catched in this method.

Now the next piece of code is just injecting some dependencies:

constructor(
  @Inject(HTTP_SERVER_REF) applicationRef: HttpServer,
  private logger: AppLoggerService
) {
  super(applicationRef);
}

You will need to send an application reference object when making an instance of the global exception filter and what you can also do is sending an instance of some logger service you have in your application so you can later log the actual exception that was thrown like this:

this.logger.error(exception);

Since I am building a very custom response, I won’t need the request object and the status, because I am populating it manually, so you can understand how much customizable it could be. That’s why they are commented.

This part of the code:

const message = (exception instanceof Error) ? exception.message : exception.message.error;

is catching all kind of exception thrown, it doesn’t matter if it is a custom NestJS Http Exception or it is just a default Javascript Error instance.

As we can see from the code above, we are setting the status code default to 500 and the following code is actually managing the response status code, here are some specific examples:

if (exception.status === HttpStatus.NOT_FOUND) {
  status = HttpStatus.NOT_FOUND;
}

if (exception.status === HttpStatus.SERVICE_UNAVAILABLE) {
  status = HttpStatus.SERVICE_UNAVAILABLE;
}

if (exception.status === HttpStatus.NOT_ACCEPTABLE) {
  status = HttpStatus.NOT_ACCEPTABLE;
}

if (exception.status === HttpStatus.EXPECTATION_FAILED) {
  status = HttpStatus.EXPECTATION_FAILED;
}

if (exception.status === HttpStatus.BAD_REQUEST) {
  status = HttpStatus.BAD_REQUEST;
}

Notice: NestJS is coming with a handy enumerator so you could use the status code as a config constant.

And at the end, we are constructing the custom response:

response
  .status(status)
  .json({
    status,
    success: false,
    data: [],
    error: message,
    message: (status === HttpStatus.INTERNAL_SERVER_ERROR) ? 'Sorry we are experiencing technical problems.' : '',
  });

Please be aware that NestJS also automatically sends an appropriate user-friendly response if you don’t need anything custom or specific and it should look something like this:

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

Now your main.ts file, the file which is in charge to accept every request, could look something like this:

import { HTTP_SERVER_REF, NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './modules/app/app.module';
import { ConfigModule } from './modules/config/config.module';
import { EnvConfigService } from './modules/config/services/env-config.service';
import { useContainer } from 'class-validator';
import { AllExceptionsFilter } from './exception-filters/all-exceptions.filter';
import { AppLoggerService } from './modules/shared/services/logger.service';
import { SharedModule } from './modules/shared/shared.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Initialize global exception filter
  const httpRef = app.get(HTTP_SERVER_REF);
  const logger = app.select(SharedModule).get(AppLoggerService, {strict: true});
  app.useGlobalFilters(new AllExceptionsFilter(httpRef, logger));

  useContainer(app.select(AppModule), { fallbackOnErrors: true });

  const configService = app.select(ConfigModule).get(EnvConfigService, {strict: true});
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(configService.get('APP_PORT'));
}
bootstrap();

The code which is important for this article is:

// Initialize global exception filter
const httpRef = app.get(HTTP_SERVER_REF);
const logger = app.select(SharedModule).get(AppLoggerService, {strict: true});
app.useGlobalFilters(new AllExceptionsFilter(httpRef, logger));

where is clear how we are sending the application reference and the logger instance to the global exception filter, using the useGlobalFilters method.

Now, if we like to test our global exception filter, we can create a simple controller called test.controller.ts and just create the following method:

@Get('test-exception-filters')
async testExceptionFilters() {
  throw new HttpException('This is not acceptable', HttpStatus.NOT_ACCEPTABLE);
}

Now, if you try to visit this route, you should get your exception message “This is not acceptable” with 406 status code and if you have a logger implemented, you should get a new error message logged.