3

I want to handle exceptions properly in my NestJS app. I have watched videos, read blogs and found some answers on stackoverflow but it is too confusing for me to implement proper exception handling.

The login method in user microservice(controller):

@GrpcMethod('UserService', 'Login')
async login(@Payload() payload: Login): Promise<any> {
    try {
        const { email, password } = payload;
        const { user, token, refreshToken } = await this.usersService.login(email, password);

        return {
            user: {
                email: user.email,
                userId: user.id,
                name: user.name,
                phone: user.phone,
            },
            token,
            refreshToken,
        };
    } catch (error) {
        throw new RpcException(error);
    }
}

The login method in user microservice(service) along with find user by email:

async findByEmail(email: string): Promise<any> {
    const user = this.userModel.findOne({ email }).exec();
    if (!user) return 'User not found!!';
    return user;
}

async login(email: string, password: string): Promise<any> {
    try {
        const user = (await this.findByEmail(email)) as User;

        const comparePassword = await passwordService.comparePasswords(password, user.password);
        if (user && comparePassword) {
            const { token, refreshToken } = await this.sessionService.createSession(user._id, {
                userId: user._id,
                type: user.type,
            });
            return { user, token, refreshToken };
        }
    } catch (error) {
        throw new Error(error);
    }
}

The controller method in API gateway file:

@Post('login')
async login(@Body() loginData: Login): Promise<any> {
    try {
        const user = await firstValueFrom(this.userService.Login(loginData));

        return sendSuccess(user, 'Log-in successful.');
    } catch (error) {
        return sendError(error.details, 404);
    }
}

I want to handle all the exceptions and throw appropriate exception. like if email is not registered then throw "Email not found" if password is wrong then throw "invalid credentials" and so on. How can I do that?

the request-response utility code:

export function sendSuccess<T>(data: T, message: string = 'Success', statusCode: number = HttpStatus.OK): ApiResponse<T> {
    return {
        status: 'success',
        statusCode,
        message,
        data,
    };
}

export function sendError<T>(message: string, statusCode: number): ApiResponse<T> {
    return {
        status: 'error',
        statusCode,
        message,
        data: null,
    };
}

export interface ApiResponse<T> {
    status: 'success' | 'error';
    statusCode: number;
    message: string;
    data: T | null;
}

@Catch()
export class AllExceptionsFilter {
    catch(exception: any, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
        const message = exception instanceof HttpException ? exception.message : 'Internal Server Error';

        response.status(status).json(sendError(message, status));
    }
}

I have registered AllExceptionsFilter in my main.ts file: app.useGlobalFilters(new AllExceptionsFilter()); but it never gets there.

John Oliver
  • 125
  • 1
  • 11

2 Answers2

1

From what I can find in docs AllExceptionsFilter should implements RpcExceptionFilter and uses RpcException for the @Catch decorator.

import { Catch, RpcExceptionFilter, ArgumentsHost } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { RpcException } from '@nestjs/microservices';

@Catch(RpcException)
export class ExceptionFilter implements RpcExceptionFilter<RpcException> {
  catch(
  exception: any, host: ArgumentsHost
): Observable<any> {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
    const message = exception instanceof HttpException ? exception.message : 'Internal Server Error';
    return throwError(() => response.status(status).json(sendError(message, status)););
  }
}

note that

The catch() method must return an Observable

Ahmed Sbai
  • 10,695
  • 9
  • 19
  • 38
  • So, There should be 2 Exception filters. one in the microservice and other should be in the API gateway? or only 1 exception filter in the gateway file(the one that you mentioned) – John Oliver Jul 09 '23 at 05:34
1

So, There should be 2 Exception filters. one in the microservice and other should be in the API gateway? or only 1 exception filter in the gateway file(the one that you mentioned)

You could actually implement both approaches (a bit mentioned in "How to handle RpcException in NestJS"):

  1. Microservices: Handle only service-specific exceptions and throw an RpcException with a specific error message or error code. They don't need to implement an RpcExceptionFilter because they don't need to catch and handle exceptions.

  2. API Gateway: Implement an RpcExceptionFilter to catch RpcException thrown by the microservices. This filter would convert the RpcException into an appropriate HTTP response. It can also handle any other exceptions that might occur within the gateway.

Microservices: specific known error conditions

You would have one at the service-level exception handling: it handles specific exceptions that are thrown when an operation in the service fails.
For example, when the findByEmail method in the usersService does not find a user, it throws an RpcException with the message 'Email not found'. That is a specific exception indicating a specific error condition in the service.

async findByEmail(email: string): Promise<any> {
    const user = await this.userModel.findOne({ email }).exec();
    if (!user) throw new RpcException('Email not found');
    return user;
}

async login(email: string, password: string): Promise<any> {
    try {
        const user = await this.findByEmail(email);
        const comparePassword = await passwordService.comparePasswords(password, user.password);
        
        if (!comparePassword) {
            throw new RpcException('Invalid credentials');
        }

        if (user && comparePassword) {
            const { token, refreshToken } = await this.sessionService.createSession(user._id, {
                userId: user._id,
                type: user.type,
            });
            return { user, token, refreshToken };
        }
    } catch (error) {
        // error is instance of RpcException here.
        throw error;
    }
}

And you would have one at the controller level, where it calls the service method. It tries to execute the Login operation of the userService, and if it fails (an exception is thrown), it catches the RpcException, extracts the message and status code from it, and sends an error response to the client.

@Post('login')
async login(@Body() loginData: Login): Promise<any> {
    try {
        const user = await firstValueFrom(this.userService.Login(loginData));
        return sendSuccess(user, 'Log-in successful.');
    } catch (error) {
        // error here is instance of RpcException
        const statusCode = error.getStatus();
        const message = error.message;
        return sendError(message, statusCode);
    }
}

API Gateway: unknown or unanticipated exceptions

Here, you can implement a global exception filter: it catches all RpcExceptions that are not caught elsewhere in the application. That can be useful for catching and handling unanticipated exceptions or exceptions that you do not have specific handling code for. That is more of a safety net, and in a well-designed application, it should not be catching many exceptions because most exceptions should be handled at the service or controller level.

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

@Catch(RpcException)
export class RpcExceptionToHttpExceptionFilter implements ExceptionFilter {
  catch(exception: RpcException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();

    // You can further customize the status code and the response based on the exception message or error code
    const status = HttpStatus.BAD_REQUEST;

    response.status(status).json({
      statusCode: status,
      message: exception.message,
    });
  }
}

Whenever an RpcException is caught, it is converted into a HTTP response with a status code of 400 (Bad Request). You can further customize this based on the exception details (e.g., differentiating between different types of application errors).

You would then use this filter in your API Gateway with app.useGlobalFilters(new RpcExceptionToHttpExceptionFilter()); (binding filter).

That approach ensures that your API Gateway translates any exceptions from your microservices into HTTP-appropriate responses, keeping the handling of HTTP concerns separated from your microservices.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250