DEV Community

Cover image for Creating a Global Exception Filter in NestJS for Robust Error Handling
Nurul Islam Rimon
Nurul Islam Rimon

Posted on • Edited on

Creating a Global Exception Filter in NestJS for Robust Error Handling

In any API, error handling is an essential aspect of ensuring smooth communication between the backend and frontend. In NestJS, error handling is made simple and scalable with the use of Exception Filters. These filters catch and handle exceptions thrown by your application, allowing you to format and customize error responses globally or at the controller level.

What is an Exception Filter in NestJS?

An Exception Filter in NestJS allows you to catch and process errors thrown during the execution of requests. It provides a centralized way to manage and respond to errors consistently across your application. Exception filters can be applied globally or at the controller level, giving you fine-grained control over error handling.

Key Features of Exception Filters:

  • Intercept errors thrown during request handling.
  • Format error responses uniformly.
  • Customize error responses, including adding detailed messages or error codes.
  • Apply globally or to specific controllers.

Creating the GlobalExceptionFilter

Let’s dive into the implementation of a GlobalExceptionFilter in NestJS. This filter will catch all errors, format them in a consistent structure, and send them back to the client.

/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { deleteFiles } from 'src/utils/file.utils';

interface ErrorMessage {
  path: string;
  message: string;
}

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
    const response = ctx.getResponse();

    // Delete uploaded files if they exist
    if (request.file) {
      deleteFiles(request.file as Express.Multer.File);
    }
    if (request.files) {
      deleteFiles(request.files as Express.Multer.File[]);
    }

    // Determine the status code based on exception type
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    let errorMessages: ErrorMessage[] = [];

    // Handle different types of errors
    if (exception?.message?.includes('Argument')) {
      console.log('prisma');
      errorMessages = this.formatPrismaError(exception);
    } else if (exception?.response?.message) {
      console.log('dto');
      errorMessages = this.formatDtoValidationError(exception.response.message);
    } else if (exception instanceof HttpException) {
      console.log('http');
      errorMessages = this.formatHttpError(exception);
    } else {
      console.log('other');
      errorMessages = this.formatOtherError(exception);
    }

    // Send the response with a proper error structure
    response.status(status).json({
      success: false,
      message: 'Validation error',
      errorMessages,
    });
  }

  formatPrismaError(exception: any): ErrorMessage[] {
    const errorMessages: ErrorMessage[] = [];

    if (exception.message.includes('Argument')) {
      const match = exception.message.match(/Argument `(\w+)` is missing/);
      if (match) {
        errorMessages.push({
          path: match[1],
          message: `${match[1]} is missing`,
        });
      }
    } else {
      if (exception.meta?.target) {
        errorMessages.push({
          path: exception.meta.target[0], // Assuming the target is a list of fields
          message: exception.message || 'Invalid value',
        });
      }
    }

    return errorMessages;
  }

  formatDtoValidationError(errors: any[]): ErrorMessage[] {
    return errors.map((error) => {
      if (typeof error === 'string') {
        return {
          path: error.split(' ')[0],
          message: error,
        };
      }
      return {
        path: error.property,
        message: error.constraints
          ? Object.values(error.constraints).join(', ')
          : 'Validation error',
      };
    });
  }

  formatHttpError(exception: HttpException): ErrorMessage[] {
    let errorMessages: ErrorMessage[] = [];

    const response = exception.getResponse() as any;

    if (typeof response === 'string') {
      errorMessages.push({
        path: 'general',
        message: response || 'An error occurred',
      });
    } else if (typeof response === 'object' && response?.message) {
      if (Array.isArray(response.message)) {
        errorMessages = response.message.map((error: any) => ({
          path: error.property || 'unknown_field',
          message: error.constraints
            ? Object.values(error.constraints).join(', ')
            : error.message || 'Validation error',
        }));
      } else {
        errorMessages = [
          {
            path: 'general',
            message: response.message || 'An error occurred',
          },
        ];
      }
    }

    return errorMessages;
  }

  formatOtherError(exception: any): ErrorMessage[] {
    const errorMessages: ErrorMessage[] = [];

    if (exception?.path && exception?.message) {
      errorMessages.push({
        path: exception.path || 'unknown_field',
        message: exception.message || 'An unexpected error occurred',
      });
    } else {
      errorMessages.push({
        path: 'unknown',
        message: exception.message || 'An unexpected error occurred',
      });
    }

    return errorMessages;
  }
}


Enter fullscreen mode Exit fullscreen mode

Applying the GlobalExceptionFilter

To ensure the GlobalExceptionFilter is used across the entire application, we need to register it in the main.ts file of the NestJS application:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './filters/global-exception.filter';

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

  // Apply the Global Exception Filter
  app.useGlobalFilters(new GlobalExceptionFilter());

  await app.listen(3000);
}
bootstrap();

Enter fullscreen mode Exit fullscreen mode

By using app.useGlobalFilters(), we ensure that any unhandled errors across the application are caught and formatted by the GlobalExceptionFilter.

Benefits of Using a GlobalExceptionFilter

  1. Consistency in Error Handling:
    By using a global exception filter, we ensure that all errors are handled and structured in the same way, making it easier for frontend developers to parse and display errors.

  2. Centralized Error Management:
    The filter helps keep error-handling logic centralized, making it easier to manage and modify. Changes to error formats or behavior only need to be made in one place.

  3. Customizable Responses:
    You can further customize the GlobalExceptionFilter to add more information, such as error codes, user-friendly messages, or even logging for debugging.

  4. Improved Developer Experience:
    Developers working on the API can focus on business logic while relying on the global exception filter to handle any unexpected errors in a standardized way.

Regards,
N I Rimon

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay