Centralized Error Handling in NestJS

A guide on how to handle all errors in a NestJS application.

The context — I had a requirement where I need to catch all errors in a place, log them [If we need to log such errors], Increment error metrics [Prometheus and prom-client for grafana dashboard]. Existing Problems — We used the common nestjs pattern to pass along the data from controllers -> service -> repositories. And since all the code block were surrounded with try-catch blocks, it was very much required that we inject [Metric Service & the Logging Service] in each and every module. Then call a generic function to perform all the necessary checks and further actions The solution — We found the solution by using a NestJs interceptor which catches Exceptions + Errors apart from Exception Filters which catch and response only to exceptions. Now once injected we removed our try catch blocks wherever seems necessary and almost 90% of our modules are free of those blocks now. In the interceptor we inject the Metric Service and the Logging Service and this would be responsible for both the operations. We can transform the error and revert it as a generic message to the user by including some attributes. To understand the flow better let us have a look on the request-response life cycle in a nestjs application image

Complete Application Flow

The top block consists of the request life cycle. And the bottom half contains the processing and response. If anything goes wrong through the full application it will always be passed through { Interceptor Post request } and this is what we can make use of. An example of the interceptor can be viewed at: https://github.com/akshay271703/nest-email-otp/blob/c6a982e3a93c1a370c3c48957bae8e6904dcdc16/src/utilities/interceptors/app.interceptor.ts This article is extended from one of my previous small project that uses an OTP based user registration. I have created the project now with Nest Js / TypeScript. And mailing is done via AWS SES. The link can be found here https://javascript.plainenglish.io/create-otp-based-user-sign-up-using-node-js-cc4defc54123.

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  HttpException,
} from "@nestjs/common";
import { Request } from "express";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";
import { MetricsService } from "src/config/metrics/metrics.service";
import { METRIC_TYPE } from "src/interfaces/metrics.interface";
import { TypeORMError } from "typeorm";
@Injectable()
export class AppInterceptor implements NestInterceptor {
  constructor(private readonly metricService: MetricsService) {}
  async intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Promise<Observable<any>> {
    return next.handle().pipe(
      catchError((err) => {
        const request: Request = context.switchToHttp().getRequest();
        if (err instanceof TypeORMError) {
          this.updateMetrics(METRIC_TYPE.DATABASE);
        } else if (err instanceof HttpException) {
          this.updateMetrics(METRIC_TYPE.APPLICATION);
        } else {
          this.updateMetrics(METRIC_TYPE.UNKNOWN);
        }
        return throwError(
          () =>
            new HttpException(
              {
                message: err?.message || err?.detail || "Something went wrong",
                timestamp: new Date().toISOString(),
                route: request.path,
                method: request.method,
              },
              err.statusCode || 500
            )
        );
      })
    );
  }

  async updateMetrics(type: METRIC_TYPE) {
    switch (type) {
      case METRIC_TYPE.DATABASE:
        this.metricService.error_database.inc();
        break;
      case METRIC_TYPE.APPLICATION:
        this.metricService.error_application.inc();
        break;
      case METRIC_TYPE.MAIL_FAILURE:
        this.metricService.mail_failure_count.inc();
        break;
      default:
        this.metricService.error_unknown.inc();
    }
  }
}

A brief summary of the usage of interceptor — Whenever my application throws an error it is caught by this interceptor and then

  • I check what type of error this is. In my case it used to be — [1] Database Error — Which I can check by using instanceof TypeORMError [2] Application Error — Which I can again check by instance f HttpException [3] Not identified. You can refine this filter but for the sake of simplicity I have added only a few.
  • I can throw a simple user friendly error details by transforming and appending to the original error message.
  • I can perform logging here and restrict what type of error I need to log.
  • I can handle my metrics here in this module.

Enjoyed this article?

Share it with your network to help others discover it

Continue Learning

Discover more articles on similar topics