Edgar Luque
Edgar Luque

Reputation: 398

How to handle mongoose error with nestjs

I followed the example from https://docs.nestjs.com/techniques/mongodb

The issue is when there is a mongoose validation error (e.g i have a schema with a required field and it isn't provided):

From games.service.ts:

  async create(createGameDto: CreateGameDto): Promise<IGame> {
    const createdGame = new this.gameModel(createGameDto);
    return await createdGame.save();
  }

The save() function returns a Promise.

Now i have this in the game.controller.ts

  @Post()
  async create(@Body() createGameDto: CreateGameDto) {
    this.gamesService.create(createGameDto);
  }

What is the best way to handle an error and then return a response with a different http status and maybe a json text? You would usually throw a HttpException but from where? I can't do that if i handle the errors using .catch() in the promise.

(Just started using the nestjs framework)

Upvotes: 20

Views: 28152

Answers (10)

buryo
buryo

Reputation: 141

In my case I had everything up and running, but at some point my MongoExceptionFilter stopt working. Turns out my Mongodb: 4.7.0 version was not matching with the Mongodb which was used in my Mongoose package. Because of this reason my exceptionFilter was not able to catch @Catch(MongoError) since the error thrown was not matching (because of different versions).

I solved the above problem by diving deep into the node_modules, check which mongodb version was used by mongoose and npm install that exact version.

Upvotes: 0

Alshoja
Alshoja

Reputation: 527

Am using a global Exception filter with a logger:(WINSTON), that catch all most every Error, including MongoDB error's, Don't know whether it's up to the standard but works for me, I followed Nest Js Doc with some of my custom needs!

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  Inject,
  LoggerService,
} from '@nestjs/common';
import { Response } from 'express';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly logger: LoggerService,
  ) {}
  catch(exp: any, host: ArgumentsHost) {
    console.log(exp);
    this.logger.error(exp);
    const context = host.switchToHttp();
    const response = context.getResponse<Response>();
    const request = context.getRequest<Request>();

    const hasKey = Object.keys(exp).length > 0 && exp.hasOwnProperty('response') ? true : false;
    const isHttpInstance = exp instanceof HttpException ? true : false;

    const validErrors = hasKey && Array.isArray(exp.response.message) ? exp.response.message : [];
    const statusCode = isHttpInstance ? exp.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
    const type = hasKey && exp.response.type ? exp.response.type : 'some_thing_went_error';
    const message = isHttpInstance ? exp.message : 'Oops Something went wrong!';
    const error = hasKey ? exp.response.error : exp;

    response.status(statusCode).json({
      message,
      type,
      validationErrors: validErrors,
      statusCode,
      error,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

This will be the response be like on MongoDB Error:

    {
    "message": "Oops Something went wrong!",
    "type": "some_thing_went_error",
    "validationErrors": [],
    "statusCode": 500,
    "error": {
        "errors": {
            "subRegion": {
                "name": "ValidatorError",
                "message": "Path `subRegion` is required.",
                "properties": {
                    "message": "Path `subRegion` is required.",
                    "type": "required",
                    "path": "subRegion"
                },
                "kind": "required",
                "path": "subRegion"
            }
        },
        "_message": "ServiceRequest validation failed",
        "name": "ValidationError",
        "message": "ServiceRequest validation failed: subRegion: Path `subRegion` is required."
    },
    "timestamp": "2022-02-08T07:43:35.962Z",
    "path": "/v1/service-request"
}

This will be the response be like on Normal Error:

{
    "message": "Bad Request Exception",
    "type": "some_thing_went_error",
    "validationErrors": [
        "isEmergency must be one of the following values: true, false",
        "isEmergency must be a string",
        "isEmergency should not be empty"
    ],
    "statusCode": 400,
    "error": "Bad Request",
    "timestamp": "2022-02-08T07:47:25.183Z",
    "path": "/v1/service-request"
}

If you want the type to be a custom one, that can be thrown from the application you can use the below code

    throw new HttpException(
      {
        status: HttpStatus.FORBIDDEN,
        type: 'otp_verification_error',
        message: 'Please verify your mobile number to complete the signUp process!',
      },
      HttpStatus.FORBIDDEN,
    );

Upvotes: 1

Richard Vergis
Richard Vergis

Reputation: 1057

You can use Error in mongoose and add it in AllExceptionFilter

Please refer to NestJS documentation for exception-filters

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  InternalServerErrorException
} from "@nestjs/common";

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

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    /**
     * @description Exception json response
     * @param message
     */
    const responseMessage = (type, message) => {
      response.status(status).json({
        statusCode: status,
        path: request.url,
        errorType: type,
        errorMessage: message
      });
    };

    // Throw an exceptions for either
    // MongoError, ValidationError, TypeError, CastError and Error
    if (exception.message.error) {
      responseMessage("Error", exception.message.error);
    } else {
      responseMessage(exception.name, exception.message);
    }
  }
}

You can add it in the main.ts like so but it really depends on your use case. You can check it in the Nest.js documentation.

async function bootstrap() {

  const app = await NestFactory.create(AppModule);

  app.useGlobalFilters(new AllExceptionsFilter());

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

Hope it helps.

Upvotes: 11

a7md0
a7md0

Reputation: 447

I am using Moongose and none of the solution here or in another questions worked for me; I followed the documentation example and did this, it did work for me.

src\filters\mongo-exception.filter.ts

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

import * as MongooseError from 'mongoose/lib/error'; // I couldn't see the error class is being exported from Mongoose

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

    let error;

    switch (exception.name) {
      case 'DocumentNotFoundError': {
        error = {
          statusCode: HttpStatus.NOT_FOUND,
          message: "Not Found"
        }
        break;
      }
      // case 'MongooseError': { break; } // general Mongoose error
      // case 'CastError': { break; }
      // case 'DisconnectedError': { break; }
      // case 'DivergentArrayError': { break; }
      // case 'MissingSchemaError': { break; }
      // case 'ValidatorError': { break; }
      // case 'ValidationError': { break; }
      // case 'ObjectExpectedError': { break; }
      // case 'ObjectParameterError': { break; }
      // case 'OverwriteModelError': { break; }
      // case 'ParallelSaveError': { break; }
      // case 'StrictModeError': { break; }
      // case 'VersionError': { break; }
      default: {
        error = {
          statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
          message: "Internal Error"
        }
        break;
      }
    }

    response.status(error.statusCode).json(error);
  }
}

src\main.ts

import { MongoExceptionFilter } from './filters/mongo-exception.filter';

async function bootstrap() {
  // .......

  app.useGlobalFilters(new MongoExceptionFilter); // Use Mongo exception filter

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

Upvotes: 3

sdykae
sdykae

Reputation: 128

I found solutions here, what I'm using is a combination of both to catch different errors

import { ArgumentsHost, Catch, ExceptionFilter, RpcExceptionFilter } from '@nestjs/common';
import { Error } from 'mongoose';
import { IDTOError } from '../errors/bad-request-exception.error';
import ValidationError = Error.ValidationError;
import { MongoError } from 'mongodb';


@Catch(MongoError)
export class MongoExceptionFilter implements ExceptionFilter {
  catch(exception: MongoError, host: ArgumentsHost) {
    // switch (exception.code) {
    //   case 11000:
    //   default: console.log(exception,'ALERT ERROR CATCHED');
    //     // duplicate exception
    //     // do whatever you want here, for instance send error to client


    //     /** MAIGOD */
    // }
    const ctx = host.switchToHttp(),
      response = ctx.getResponse();

    return response.status(400).json(<IDTOError>{
      statusCode: 400,
      createdBy: 'ValidationErrorFilter, Schema or Model definition',
      errors: exception,
    });

  }
}

@Catch(ValidationError)
export class ValidationErrorFilter implements RpcExceptionFilter {

  catch(exception: ValidationError, host: ArgumentsHost): any {

    const ctx = host.switchToHttp(),
      response = ctx.getResponse();

    return response.status(400).json(<IDTOError>{
      statusCode: 400,
      createdBy: 'ValidationErrorFilter, Schema or Model definition',
      errors: exception.errors,
    });
  }
}

Upvotes: 1

BoSkiv
BoSkiv

Reputation: 811

Nail it today

validation-error.filter.ts:

import { ArgumentsHost, Catch, RpcExceptionFilter } from '@nestjs/common';
import { Error } from 'mongoose';
import ValidationError = Error.ValidationError;

@Catch(ValidationError)
export class ValidationErrorFilter implements RpcExceptionFilter {

  catch(exception: ValidationError, host: ArgumentsHost): any {

    const ctx = host.switchToHttp(),
      response = ctx.getResponse();

    return response.status(400).json({
      statusCode: 400,
      createdBy: 'ValidationErrorFilter',
      errors: exception.errors,
    });
  }
}

main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationErrorFilter } from './validation-error.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new ValidationErrorFilter());
  await app.listen(process.env.PORT || 3000);
}
bootstrap();

result:

{
  "statusCode": 400,
  "createdBy": "ValidationErrorFilter",
  "errors": {
    "dob": {
      "properties": {
        "message": "Path `dob` is required.",
        "type": "required",
        "path": "dob"
      },
      "kind": "required",
      "path": "dob"
    },
    "password": {
      "properties": {
        "message": "Path `password` is required.",
        "type": "required",
        "path": "password"
      },
      "kind": "required",
      "path": "password"
    }
  }
}

Upvotes: 8

Purushotam Kumar
Purushotam Kumar

Reputation: 1092

I did some research I found this one is working. Create One Mongo Exception Filter as below

import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from "@nestjs/common";
import { MongoError } from 'mongodb';
import { Response } from 'express';

@Catch(MongoError)
export class MongoExceptionFilter implements ExceptionFilter {

    catch(exception: MongoError, host: ArgumentsHost) {
        switch (exception.code) {
            case 11000:
                const ctx = host.switchToHttp();
                const response = ctx.getResponse<Response>();
                response.statusCode = HttpStatus.FORBIDDEN;
                response
                    .json({
                        statusCode: HttpStatus.FORBIDDEN,
                        timestamp: new Date().toISOString(),
                        message: 'You are already registered'
                    });
        }
    }
}

And Do not forget to define the controller method as follows:

@UseFilters(MongoExceptionFilter)
  @Post('signup')
  @HttpCode(HttpStatus.OK)
  async createUser(@Body() createUserDto: CreateUserDto) {
    await this.userService.create(createUserDto);
  }

Hope this helps someone. Cheers!

Upvotes: 1

Meheret
Meheret

Reputation: 29

use try/catch

async getUser(id: string, validateUser ?: boolean): Promise<Users> {
    try {
      const user = await this.userModel.findById(id).exec();
      if(!user && validateUser) {
        throw new UnauthorizedException();
      }else if(!user) {
        throw new HttpException(`Not found this id: ${id}`, HttpStatus.NOT_FOUND)
      }
      return user;
    } catch (err) {
      throw new HttpException(`Callback getUser ${err.message}`, HttpStatus.BAD_REQUEST);
    }

Upvotes: 2

1FpGLLjZSZMx6k
1FpGLLjZSZMx6k

Reputation: 1067

First, you forgot to add return in your create method inside the controller. This is a common, very misleading mistake I made a thousand of times and took me hours to debug.

To catch the exception:

You could try to catch MongoError using @Catch.

For my projects I'm doing the following:

import { ArgumentsHost, Catch, ConflictException, ExceptionFilter } from '@nestjs/common';
import { MongoError } from 'mongodb';

@Catch(MongoError)
export class MongoExceptionFilter implements ExceptionFilter {
  catch(exception: MongoError, host: ArgumentsHost) {
    switch (exception.code) {
      case 11000:
        // duplicate exception
        // do whatever you want here, for instance send error to client
    }
  }
}

You can then just use it like this in your controller (or even use it as a global / class scoped filter):

import { MongoExceptionFilter } from '<path>/mongo-exception.filter';

@Get()
@UseFilters(MongoExceptionFilter)
async findAll(): Promise<User[]> {
  return this.userService.findAll();
}

(Duplicate exception doesn't make sense here in a findAll() call, but you get the idea).

Further, I would strongly advise to use class validators, as described here: https://docs.nestjs.com/pipes

Upvotes: 20

Chau Tran
Chau Tran

Reputation: 5088

What I am doing in my application is to use Exception Filters (https://docs.nestjs.com/exception-filters) and try/catch:

  async create(createGameDto: CreateGameDto): Promise<IGame> {
    try {
      const createdGame = new this.gameModel(createGameDto);
      return await createdGame.save();
    } catch (e) {
       // the e here would be MongoError
       throw new InternalServerException(e.message);
    }
  }

Upvotes: 0

Related Questions