如何在NestJS微服务中实现精准的异常处理?
问题分析与解决方案
一、实现针对性异常区分
1. 修复服务层核心逻辑并抛出明确异常
首先修正findByEmail的异步调用bug,并创建自定义异常类用于区分不同错误场景:
// 自定义异常类 export class UserNotFoundException extends Error { constructor() { super('邮箱未注册'); this.name = 'UserNotFoundException'; } } export class InvalidCredentialsException extends Error { constructor() { super('无效凭据'); this.name = 'InvalidCredentialsException'; } } // 服务层findByEmail方法 async findByEmail(email: string): Promise<User> { // 必须添加await,否则user是Promise对象,!user永远为false const user = await this.userModel.findOne({ email }).exec(); if (!user) { throw new UserNotFoundException(); } return user; } // 服务层login方法 async login(email: string, password: string): Promise<{ user: User; token: string; refreshToken: string }> { const user = await this.findByEmail(email); const comparePassword = await passwordService.comparePasswords(password, user.password); if (!comparePassword) { throw new InvalidCredentialsException(); } const { token, refreshToken } = await this.sessionService.createSession(user._id, { userId: user._id, type: user.type, }); return { user, token, refreshToken }; }
2. GRPC控制器封装对应GRPC状态码
在GRPC控制器中根据异常类型,封装带状态码的RpcException:
import { Status } from '@grpc/grpc-js'; @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) { let statusCode = Status.INTERNAL; if (error instanceof UserNotFoundException) { statusCode = Status.NOT_FOUND; } else if (error instanceof InvalidCredentialsException) { statusCode = Status.UNAUTHENTICATED; } throw new RpcException({ code: statusCode, message: error.message }); } }
3. API网关转换异常为HTTP格式并抛出
网关控制器中解析GRPC异常,转换为Nest的HttpException抛出(让全局过滤器捕获):
import { Status } from '@grpc/grpc-js'; import { HttpException, HttpStatus } from '@nestjs/common'; @Post('login') async login(@Body() loginData: Login): Promise<any> { try { const user = await firstValueFrom(this.userService.Login(loginData)); return sendSuccess(user, '登录成功'); } catch (error) { let statusCode = HttpStatus.INTERNAL_SERVER_ERROR; let message = '服务器内部错误'; if (error.code === Status.NOT_FOUND) { statusCode = HttpStatus.NOT_FOUND; message = error.details; } else if (error.code === Status.UNAUTHENTICATED) { statusCode = HttpStatus.UNAUTHORIZED; message = error.details; } // 抛出异常,交给全局过滤器处理 throw new HttpException(message, statusCode); } }
二、修复全局异常过滤器不生效问题
1. 调整过滤器适配异常解析逻辑
优化AllExceptionsFilter,确保正确解析HttpException的响应内容:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; import { Response } from 'express'; @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); let status: number; let message: string; if (exception instanceof HttpException) { status = exception.getStatus(); // 兼容HttpException的字符串或对象格式响应 const exceptionResponse = exception.getResponse(); message = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message; } else { status = HttpStatus.INTERNAL_SERVER_ERROR; message = exception.message || '服务器内部错误'; } response.status(status).json(sendError(message, status)); } }
2. 确保过滤器正确注册且无局部覆盖
- 网关
main.ts中注册全局过滤器的代码保持不变,但需确保控制器/方法未使用@UseFilters()装饰器(局部过滤器会覆盖全局过滤器):
app.useGlobalFilters(new AllExceptionsFilter());
- 核心前提:网关控制器必须抛出异常,而非直接
return sendError,否则过滤器无法捕获异常。
三、其他关键修复总结
- 修复
findByEmail方法缺少await的bug,否则永远无法触发“用户未找到”逻辑; - 补充登录流程中密码不匹配的异常抛出逻辑,避免无响应或未知错误。
内容的提问来源于stack exchange,提问作者John Oliver
相关产品推荐
相关产品推荐

