78

While using NestJS to create API's I was wondering which is the best way to handle errors/exception. I have found two different approaches :

  1. Have individual services and validation pipes throw new Error(), have the controller catch them and then throw the appropriate kind of HttpException(BadRequestException, ForbiddenException etc..)
  2. Have the controller simply call the service/validation pipe method responsible for handling that part of business logic, and throw the appropriate HttpException.

There are pros and cons to both approaches:

  1. This seems the right way, however, the service can return Error for different reasons, how do I know from the controller which would be the corresponding kind of HttpException to return?
  2. Very flexible, but having Http related stuff in services just seems wrong.

I was wondering, which one (if any) is the "nest js" way of doing it?

How do you handle this matter?

frederj
  • 1,483
  • 9
  • 20
Aaron Ullal
  • 4,855
  • 8
  • 35
  • 63

5 Answers5

61

Let's assume your business logic throws an EntityNotFoundError and you want to map it to a NotFoundException.

For that, you can create an Interceptor that transforms your errors:

@Injectable()
export class NotFoundInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // next.handle() is an Observable of the controller's result value
    return next.handle()
      .pipe(catchError(error => {
        if (error instanceof EntityNotFoundError) {
          throw new NotFoundException(error.message);
        } else {
          throw error;
        }
      }));
  }
}

You can then use it by adding @UseInterceptors(NotFoundInterceptor) to your controller's class or methods; or even as a global interceptor for all routes. Of course, you can also map multiple errors in one interceptor.

Try it out in this codesandbox.

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
22

Nest Js provides an exception filter that handles error not handled in the application layer, so i have modified it to return 500, internal server error for exceptions that are not Http. Then logging the exception to the server, then you can know what's wrong and fix it.

import 'dotenv/config';
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';

@Catch()
export class HttpErrorFilter implements ExceptionFilter {
  private readonly logger : Logger 
  constructor(){
    this.logger = new Logger 
  }
  catch(exception: Error, host: ArgumentsHost): any {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
    const response = ctx.getResponse();

    const statusCode = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
    const message = exception instanceof HttpException ?  exception.message || exception.message?.error: 'Internal server error'

    const devErrorResponse: any = {
      statusCode,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      errorName: exception?.name,
      message: exception?.message
    };

    const prodErrorResponse: any = {
      statusCode,
      message
    };
    this.logger.log( `request method: ${request.method} request url${request.url}`, JSON.stringify(devErrorResponse));
    response.status(statusCode).json( process.env.NODE_ENV === 'development'? devErrorResponse: prodErrorResponse);
  }
}
Ukpa Uchechi
  • 604
  • 1
  • 6
  • 10
  • 3
    Nice implementation! I think you can also use NestJS dependency injection syntax, so you don't have to declare a private property "logger" and then instantiate it. You can just use "private readonly logger : Logger" inside the constructor and it is instantiated automatically. – Marcus Castanho Jan 06 '22 at 14:25
  • 1
    Thank you, noted will implement and update the answer. – Ukpa Uchechi Jan 06 '22 at 14:34
  • At least for my use case, this is the best answer. – noamtm May 15 '22 at 13:53
  • 1
    FYI @MarcusCastanho - it seems you can't use NestJS DI, at least when using a global filter -- because you have to instantiate the filter yourself. – noamtm May 15 '22 at 13:54
  • To return the correct error message you need to do something like this: ```const message = exception instanceof HttpException ? exception.getResponse()?.['message'] ? exception.getResponse()?.['message'] : exception['message'] : 'Internal server error';``` – LuscaDev Jun 09 '22 at 15:08
6

You may want to bind services not only to HTTP interface, but also for GraphQL or any other interface. So it is better to cast business-logic level exceptions from services to Http-level exceptions (BadRequestException, ForbiddenException) in controllers.

In the simpliest way it could look like

import { BadRequestException, Injectable } from '@nestjs/common';

@Injectable()
export class HttpHelperService {
  async transformExceptions(action: Promise<any>): Promise<any> {
    try {
      return await action;
    } catch (error) {
      if (error.name === 'QueryFailedError') {
        if (/^duplicate key value violates unique constraint/.test(error.message)) {
          throw new BadRequestException(error.detail);
        } else if (/violates foreign key constraint/.test(error.message)) {
          throw new BadRequestException(error.detail);
        } else {
          throw error;
        }
      } else {
        throw error;
      }
    }
  }
}

and then

Alexey Petushkov
  • 2,010
  • 19
  • 19
1

You could also use a factory or handler to when controller catch the exception (error or domain error) its map it to another HttpException.

@Controller('example')
export class ExampleController {

  @Post('make')
  async make(@Res() res, @Body() data: dataDTO): Promise<any> {
   
    try {
      //process result...
       return res.status(HttpStatus.OK).json(result);
    } catch (error) {
      throw AppErrorHandler.createHttpException(error); //<---here is the error type mapping
    };
  };

};
0

If you try to send a bad request error, the below code might help you:

import { Controller, Post, UploadedFiles, UseInterceptors, Body, Get } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';



@Post('/multiple')
  @UseInterceptors(FilesInterceptor('files'))
  async uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body: any) {
    console.log('body :', body);
    if (!files || !files.length) {
      throw new BadRequestException('files should have at least one object');
    }
    const req: FileDataReq = {
      files,
      ...body,
    };
    return req;
  }
Shubham Verma
  • 8,783
  • 6
  • 58
  • 79