Blog Cover

How to create an exception filter in NestJS

Author profile image
Aitor Alonso

Nov 25, 2022

4 min read

Using a powerful feature of NestJS, exception filters, we can create a layer that will handle all the exceptions thrown by our application and return a custom error response. This is useful to provide a consistent error response format, and to hide sensitive information from the server, such as implementation details.

What is an exception filter?

NestJS comes with a built-in exceptions layer which is responsible for processing all unhandled exceptions across an application. However, while the base (built-in) exception filter can automatically handle many cases for us, we may want full control over the exceptions layer. For example, imagine we want to provide a custom 404 error when a resource is not found in our API, with a human readable error explaining what happened, instead of the default 500: Internal Server Error. We may want to add logging or use a different JSON schema based on some dynamic factors. Exception filters are designed for exactly this purpose. They let us control the exact flow of control and the content of the response sent back to the client.

How to create an exception filter?

What I use to do is to create all errors in my application as custom classes. All those error classes extend from a generic DomainError class, called so because I follow the Domain Driven Design approach when developing. This is how this class looks like:

// file DomainErrorCode.ts (example error codes)
export enum DomainErrorCode {
  USER_NOT_FOUND = 'USER_NOT_FOUND_ERROR',
  INVALID_STATUS = 'INVALID_STATUS_ERROR',
}

// file DomainError.ts
import { DomainErrorCode } from './DomainErrorCode'

export class DomainError extends Error {
  public readonly code: DomainErrorCode

  constructor(message: string, code: DomainErrorCode) {
    super(message)
    this.code = code
  }
}

This way, whenever there should be an exception thrown in my application, I always throw a custom error, like this:

// file UserNotFoundError.ts
import { DomainError } from './DomainError'
import { DomainErrorCode } from './DomainErrorCode'

export class UserNotFoundError extends DomainError {
  constructor(id: number) {
    super(`Couldn't find a user with ID ${id}`, DomainErrorCode.USER_NOT_FOUND)
  }
}

// file UserService.ts
import { UserNotFoundError } from './UserNotFoundError'

/* ... */

const id = 1
const user = usersRepository.findById(id)
if (!user) {
  throw new UserNotFoundError(id)
}

Using custom errors as shown, instead of just doing throw new Error('message'), gives us the advantage to handle them in a custom way. And this is where our exception filter comes into action. Let's take a look at the following code.

//file domainErrorToHttpStatusCode.ts
import { HttpStatus } from '@nestjs/common'
import { DomainErrorCode } from './DomainErrorCode'

export const domainErrorToHttpStatusCode: Record<DomainErrorCode, HttpStatus> = {
  [DomainErrorCode.USER_NOT_FOUND]: HttpStatus.NOT_FOUND,
  [DomainErrorCode.INVALID_STATUS]: HttpStatus.CONFLICT,
}

// file DomainErrorFilter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'
import { Response } from 'express'
import { DomainError } from './DomainError'
import { domainErrorToHttpStatusCode } from './domainErrorToHttpStatusCode'

@Catch(DomainError)
export class DomainErrorFilter implements ExceptionFilter {
  // This filter catches unhandled domain exceptions and returns a predefined
  // well-formatted JSON response with a human-readable error and a semantically
  // correct HTTP status code that can be handled programmatically by the client.
  catch(exception: DomainError, host: ArgumentsHost): void {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const httpStatusCode = domainErrorToHttpStatusCode[exception.code]

    response.status(httpStatusCode).end(
      JSON.stringify({
        code: exception.code,
        message: exception.message,
      })
    )
  }
}

This way, whenever an exception is thrown in my application, we will respond with a custom response like this:

{
  "code": "USER_NOT_FOUND_ERROR",
  "message": "Couldn't find a user with ID 1"
}

And HTTP status code of 404. But remember! The above filter only works for custom errors that extends from DomainError, as we have defined the @Catch(DomainError) decorator. Other types of errors (custom errors that doesn't extend from DomainError, or native errors like TypeError) will be handled by the default exception filter, and will return an HTTP 500 error with a generic Internal Server Error message.

Alway write your customs error classes! I consider this a good practice, because it allows us to have a consistent error response format, and to have fully control over our error handling. I try to always generate custom error clases for the different errors of my application, and to handle them with custom exception filters.

This is just a silly example to return a custom response format, but you can imagine how powerful this can be. For example, you can log the error to an external logging system, send an email or a notification to the developers, or any other thing you could think about. The possibilities are endless!