NestJS如何结构化代码优雅处理应用错误?
NestJS 错误处理最佳实践(消除重复try/catch样板)
核心原则(先解决你的选型疑问)
- 异常抛出遵循谁发现问题谁抛出:Service层检测到业务规则不满足(比如用户不存在、密码错误)就直接抛语义化异常,不需要传到Resolver/Controller层再抛;Controller/Resolver只负责参数校验、调用Service、返回响应,不重复实现业务错误判断。
- 通用错误逻辑(日志打印、错误格式化、未知错误转500响应)统一交给全局异常过滤器实现,全应用只需要写一次,完全不需要在每个业务方法里重复写try/catch,符合DRY原则。拦截器适合做请求前后的通用切面逻辑(比如响应序列化、性能打点),不适合做错误兜底处理。
fn().then().catch()和async/await + try/catch只是Promise的不同写法,本质没有区别,换写法不会减少样板代码。统一用async/await可读性更高,配合全局过滤器绝大多数场景不需要写catch块。- 只有当你需要对特定错误做降级处理(比如调用第三方接口失败返回默认值)时,才需要在业务代码里写局部try/catch,否则不要手动捕获异常。
你当前代码的问题
- 每个方法都包try/catch实现日志打印+抛500错误,属于完全可抽离的重复样板代码。
- catch块逻辑存在bug:你主动抛出的400状态码业务异常,会被catch块捕获,直接覆盖成500服务端错误,前端无法拿到正确的错误提示和状态码。
注意:你当前的catch块是"吞错"逻辑,会丢失原始错误的语义,线上排查问题时很难定位具体错误原因。
重构实现
第一步:精简业务方法,移除冗余try/catch
业务逻辑里只保留分支判断和对应异常抛出,不需要手动捕获错误:
async validateUser( email: string, plainTextPassword: string, ): Promise<User | null> { const user = await this.usersRepository.findOne({ email }); if (!user) { throw new BadRequestException("Wrong credentials provided"); } const isMatch = await this.verifyPassword( plainTextPassword, user.password, ); if (!isMatch) { throw new BadRequestException("Wrong credentials provided"); } await this.usersRepository.filter(user); return user; } async verifyPassword(plainTextPassword: string, hashedPassword: string) { return bcrypt.compare(plainTextPassword, hashedPassword); }
注:上面用的BadRequestException是Nest内置的标准异常类,等价于new HttpException(msg, HttpStatus.BAD_REQUEST),写法更简洁。Nest内置了BadRequestException、UnauthorizedException、NotFoundException、InternalServerErrorException等常用异常类,可直接使用。
第二步:实现全局异常过滤器统一兜底
全局异常过滤器会自动捕获整个应用所有未被手动处理的异常,在这里统一实现日志打印、错误格式化、未知错误转500响应的逻辑,全应用生效:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common'; import { Response, Request } from 'express'; @Catch() export class AllExceptionsFilter implements ExceptionFilter { private readonly logger = new Logger(AllExceptionsFilter.name); catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); // 区分主动抛出的HTTP异常和未知运行时错误 const isHttpException = exception instanceof HttpException; const httpStatus = isHttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const responseMsg = isHttpException ? exception.getResponse() : "Something went wrong"; // 统一打印错误日志,不需要每个业务方法单独写 this.logger.error( `[请求错误] 路径:${request.url} 方法:${request.method} 信息:${exception instanceof Error ? exception.message : exception}`, exception instanceof Error ? exception.stack : '' ); // 统一返回标准化错误响应 response.status(httpStatus).json({ statusCode: httpStatus, message: responseMsg, timestamp: new Date().toISOString(), path: request.url, }); } }
第三步:注册全局过滤器
在应用入口文件main.ts中注册过滤器,即可对所有接口生效:
async function bootstrap() { const app = await NestFactory.create(AppModule); // 注册全局异常过滤器 app.useGlobalFilters(new AllExceptionsFilter()); await app.listen(3000); } bootstrap();
额外优化建议
- 如果是GraphQL场景,全局异常过滤器需要适配GraphQL的ExecutionContext,逻辑和HTTP场景一致,都是统一捕获异常后返回格式化的错误信息。
- 对于数据库操作、第三方服务调用产生的未知运行时错误,不需要手动捕获,全局过滤器会自动记录日志并返回500响应。
- 局部try/catch捕获异常后,如果不需要做降级处理,一定要把异常重新抛出,不要吞错,否则全局过滤器无法感知到错误,不会打印日志也不会返回正确的错误响应。
内容的提问来源于stack exchange,提问作者wongx
相关产品推荐
相关产品推荐

