3 Ways to Handle Errors in NestJS

Updated: January 1, 2024 By: Guest Contributor Post a comment

Learn some different approaches to dealing with errors and exceptions in NestJS.

1. Using Built-in Exception Filters

Solution description: NestJS provides built-in exception filters that allow you to catch and format exceptions before sending them to the client. These predetermined filters can handle different types of HTTP exceptions. Custom exception filters can be created by implementing the ExceptionFilter interface.

The main steps:

  • Create a custom exception filter by implementing the ExceptionFilter interface.
  • Use the @Catch() decorator to specify which exceptions the filter should catch.
  • Inject the custom filter into your application through the APP_FILTER token or use it in a route handler with the @UseFilters() decorator.

Here’s a simple example:

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

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

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

Pros:

  • Easy to implement and use.
  • Allows centralizing error handling logic.
  • Improves readability by abstracting the error handling away from the business logic.

Cons:

  • May become complex if different routes require different error handling strategies.

2. Using Interceptors

Solution description: Interceptors allow you to bind extra logic before or after method execution. They can also transform the result returned from a function or transform the exception thrown from it.

The process is as follows:

  • Create a class that implements the NestInterceptor interface.
  • Implement the intercept method to catch and handle or transform exceptions.
  • Bind the interceptor globally or locally to a controller or a specific route handler using the @UseInterceptors() decorator.

A small code example:L

import { CallHandler, ExecutionContext, NestInterceptor, Injectable, UseInterceptors } from '@nestjs/common';
import { catchError } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    return next
      .handle()
      .pipe(
        catchError(err => throwError(() => new CustomException(err.message)))
      );
  }
}

@UseInterceptors(ErrorsInterceptor)
export class SomeController {}

Pros:

  • Offers the flexibility of modifying the response and error together.
  • Useful for logging or additional error handling that applies to multiple routes.

Cons:

  • Requires understanding of RxJS to manipulate the response pipeline.

3. Using AOP with Custom Decorators

Solution description: Aspect-oriented programming (AOP) allows you to apply cross-cutting concerns separately from the main business logic. In NestJS, you can use custom decorators to handle errors in a more modular and reusable way.

TL;DR:

  • Create a custom decorator that will intercept method calls and encapsulate the error handling logic.
  • Ensure the custom decorator captures exceptions and handles them accordingly.
  • Apply the decorator to service methods or controller actions where you want to manage errors.

Code example:

import { Injectable, UseInterceptors, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { catchError } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';

export function CatchCustomException(): MethodDecorator {
  return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
      try {
        return originalMethod.apply(this, args);
      } catch (error) {
        throw new CustomErrorException(error.message);
      }
    };
  };
}

@Injectable()
export class SomeService {

  @CatchCustomException()
  someMethod() {
    // method logic
  }
}

Pros:

  • Enables clean separation of error handling from the main logic.
  • Decorator can be reused across different methods and classes.

Cons:

  • May obscure the control flow, making it harder to understand for developers not familiar with AOP.

Final Words

Each of these error handling strategies in NestJS has its own strengths and weaknesses. Choosing the right strategy will often depend on the specific requirements of your project, familiarity with design patterns and programming paradigms like AOP, as well as the complexity of your error-handling logic. Built-in exception filters provide an easy and integrated way to handle exceptions globally. Interceptors offer additional flexibility and can manipulate responses alongside errors. Custom decorators and AOP give you the power to declutter your business logic by separating concerns but can add a layer of complexity to your code structure.

In summary, understanding and implementing efficient error-handling strategies are crucial for the development of robust and user-friendly web applications. By mastering these techniques in NestJS, you can ensure that your applications not only handle errors gracefully but also provide insightful feedback to the clients, improving overall user experience.