CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
NestJS 实战入门/课时/异常与过滤器
RawBlame
课时目录12 节

NestJS 实战入门

  1. 01NestJS 是什么
  2. 02项目创建与结构
  3. 03Module 与依赖注入
  4. 04Controller 与路由
  5. 05Service 层
  6. 06Pipe 与校验
  7. 07Guard 与认证
  8. 08ConfigModule
  9. 09数据库接入入门
  10. 10异常与过滤器
  11. 11测试
  12. 12综合练习

异常与过滤器

HttpException、ExceptionFilter、统一错误 JSON。

30 分钟

异常与过滤器

这一课你将能做到

  1. 在 Service 中抛出 NotFoundException、ConflictException 等语义异常,并自定义带业务码的异常
  2. 编写全局 ExceptionFilter,把所有错误统一成固定 JSON 外壳
  3. 区分「可预期业务错误」与「未知 500」,日志保留堆栈、响应体对客户端脱敏

为什么要学

前端和调用方需要稳定的错误形状:看到固定字段,才能写统一的提示逻辑(比如弹出 message、按 code 跳登录)。如果有的接口返回字符串、有的返回 HTML 错误页、有的直接挂掉,前端就得为每个接口写特例。

Nest 内置了 HttpException 异常体系,再配合 ExceptionFilter 在出口统一拦截——业务代码只管抛出语义异常,过滤器决定最终长什么样。这对应 Express 的统一错误处理中间件,也完全对应 Spring 的 @ControllerAdvice 加 @ExceptionHandler。

先建立图像

Service 抛出 NotFoundException
    → Nest 默认映射为 404 JSON
    → 被全局 AllExceptionsFilter 接住
        → 统一写成 { code, message, statusCode, path, timestamp }

未知 Error(空指针、第三方崩溃……)
    → 过滤器按 500 处理
    → 服务端日志记 stack;响应体只给通用文案

常用内置异常:

异常类状态码使用场景
BadRequestException400参数格式错误(Pipe 默认用它)
UnauthorizedException401未登录、Token 无效
ForbiddenException403已登录但权限不足
NotFoundException404资源不存在
ConflictException409资源冲突,如重复题干
UnprocessableEntityException422格式正确但语义无法处理

跟着做

步骤 1:在业务中抛语义异常

确认 QuestionsService 的写法:

if (!question) {
  throw new NotFoundException(`题目 ${id} 不存在`);
}

也可以在构造 HttpException 时携带自定义响应体:

import { HttpException, HttpStatus } from "@nestjs/common";

throw new HttpException(
  {
    code: "QUESTION_NOT_PUBLISHED",
    message: "题目尚未发布,不能作答",
  },
  HttpStatus.UNPROCESSABLE_ENTITY, // 422
);

请求一次不存在的题目,默认会看到 Nest 自带的三个字段:statusCode、message、error。

步骤 2:定义业务异常基类

新建 src/common/exceptions/business.exception.ts:

import { HttpException, HttpStatus } from "@nestjs/common";

export class BusinessException extends HttpException {
  constructor(
    code: string,
    message: string,
    status: HttpStatus = HttpStatus.BAD_REQUEST,
  ) {
    super({ code, message }, status);
  }
}

再定义两个具体异常,业务语义一目了然:

export class QuestionNotPublishedException extends BusinessException {
  constructor(questionId: number) {
    super(
      "QUESTION_NOT_PUBLISHED",
      `题目 ${questionId} 尚未发布`,
      HttpStatus.UNPROCESSABLE_ENTITY,
    );
  }
}

export class DuplicateSubmissionException extends BusinessException {
  constructor() {
    super("DUPLICATE_SUBMISSION", "同一题目的作答已提交,请勿重复", 409);
  }
}

Service 中直接 throw new QuestionNotPublishedException(id),比手写状态码和 message 数组更不容易出错。

步骤 3:写全局异常过滤器

新建 src/common/filters/all-exceptions.filter.ts:

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  Logger,
} from "@nestjs/common";
import { Request, Response } from "express";

@Catch() // 不写参数 = 捕获一切,包括裸 Error
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger("Exception");

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();

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

    const body = this.extractBody(exception);

    if (status >= 500) {
      // 人话:日志要细节,方便定位;给客户端要克制
      this.logger.error(
        `${req.method} ${req.url} → ${status}`,
        exception instanceof Error ? exception.stack : String(exception),
      );
    }

    res.status(status).json({
      code: body.code ?? this.defaultCode(status),
      message: body.message,
      statusCode: status,
      path: req.url,
      timestamp: new Date().toISOString(),
    });
  }

  private extractBody(exception: unknown): {
    code?: string;
    message: string | string[];
  } {
    if (exception instanceof HttpException) {
      const response = exception.getResponse();
      if (typeof response === "string") {
        return { message: response };
      }
      const obj = response as Record<string, unknown>;
      return {
        code: typeof obj.code === "string" ? obj.code : undefined,
        message:
          (obj.message as string | string[] | undefined) ?? exception.message,
      };
    }
    // 人话:未知错误不把内部信息透出
    return { message: "服务开小差了,请稍后再试" };
  }

  private defaultCode(status: number): string {
    return `HTTP_${status}`;
  }
}

步骤 4:注册过滤器(推荐用依赖注入方式)

方式一,main.ts 中直接挂(过滤器无需注入依赖时够用):

app.useGlobalFilters(new AllExceptionsFilter());

方式二,在 AppModule 通过 APP_FILTER 注册,过滤器内可注入日志、监控等 Service,更推荐:

import { Module } from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";
import { AllExceptionsFilter } from "./common/filters/all-exceptions.filter";

@Module({
  // imports 不变
  providers: [{ provide: APP_FILTER, useClass: AllExceptionsFilter }],
})
export class AppModule {}

再次请求不存在的题目,响应体会带你定义的 code、path、timestamp,团队把这个形状写进接口文档,前端只需要解析一种结构。

步骤 5:制造一次未知异常验证脱敏

临时在某个 Service 方法里写 throw new Error("database password is xyz"),触发请求。

应看到:响应状态 500,message 是通用文案,不包含密码和堆栈;终端日志里完整保留了 stack。验证后删掉这行。结论:内部细节只进日志,不进响应体,堆栈、SQL、内网地址都可能被攻击者利用。

步骤 6:与校验错误对齐

故意 POST 一个非法 DTO(如 options 为空数组),观察过滤器输出:message 是 class-validator 返回的字符串数组,逐条说明字段问题。

前端处理约定:message 为数组时展示第一条(或逐条渲染在表单字段旁);为字符串时直接弹窗。由于 ValidationPipe 抛的也是 HttpException,校验错误和业务错误天然走同一个过滤器出口。

步骤 7:对照 Spring @ControllerAdvice

在笔记里写下三行对应关系:

抛异常:Service 中 throw new XxxException
统一捕获:@ControllerAdvice  ↔  @Catch() 全局 Filter
固定响应:返回统一 Result 对象 ↔ 返回统一 JSON 外壳

换语言不换套路,这也是本站反复对照的原因。

讲清楚要点

  1. 错误形状就是接口契约:字段稳定、含义固定,调用方才好编程。
  2. 可预期失败用 HttpException 子类:状态码即语义,不要一切失败都返回 200 再在 body 里标成功与否。
  3. 未知错误必须脱敏:500 只回通用文案,堆栈和内部信息只写日志。
  4. 过滤器是横切层:天然适合挂日志、监控告警、traceId 透传等能力。
  5. 校验错误自动收口:Pipe 抛异常,过滤器统一处理,不需要为校验单写出口。
  6. 自定义异常承载业务码:code 字段比 HTTP 状态码更细,前端可据此做差异化处理。

你可能会懵的地方

  • 挂了全局过滤器,Nest 默认异常层还在吗? 在,但最终响应内容由你的过滤器决定;@Catch() 捕获全部,@Catch(HttpException) 只处理 HTTP 异常、其余仍走默认逻辑。

  • getResponse() 为什么有时是对象有时是字符串? 内置异常默认返回 { message, error, statusCode },校验错误中 message 还是数组;自定义异常可以返回任意结构。过滤器要做归一化。

  • 异步方法里抛异常能被捕获吗? 可以。Nest 会处理 Promise 的 rejection;不要写空 catch 把错误吞掉,那样请求会一直挂起。

  • 过滤器执行顺序如何控制? 多个过滤器按绑定范围从窄到宽、同范围按注册顺序执行;一般一个全局 AllExceptionsFilter 足够,避免层层嵌套。

  • API 返回 HTML 错误页行不行? API 服务只返回 JSON;HTML 错误页由前端路由或网关提供。初学者最容易忽略这一点,而混着返回收割的就是前端调试时间。

错法对照

错法问题正解
catch (e) { return null }静默失败,问题被掩盖上抛或转成语义异常
500 响应带 stack / SQL / 内网信息信息泄露日志保留细节,响应通用文案
Controller 手写 res.status(404).json(...)各接口格式不统一Service 抛异常,过滤器统一收口
成功失败都返回 200 + { success }HTTP 语义失效用真实状态码表达结果
每个模块定义不同错误字段名前端每接一个接口崩一次全局统一错误外壳

卡点急救

  1. 过滤器不生效:确认注册方式与范围;同时使用 useGlobalFilters 和 APP_FILTER 会重复执行,只保留一种。
  2. 自定义异常仍返回默认结构:检查是否继承 HttpException 并正确调用 super(response, status);裸 Error 一律被当 500。
  3. 生产排查看不到错误详情:详情应在服务端日志/监控平台检索;用响应中的 path、timestamp、traceId 对应日志定位。

面试常问

  • NestJS 的异常过滤器是干什么的? ExceptionFilter 在异常抛出后接管响应,用 @Catch 指定捕获范围,可按异常类型生成统一格式的错误响应;全局注册后所有错误统一出口,对应 Spring 的 @ControllerAdvice。

  • 业务异常和系统异常在处理上有何不同? 业务异常是可预期的(资源不存在、冲突、校验失败),抛 HttpException 子类并透出明确信息;系统异常是未预期的程序错误,按 500 处理,响应脱敏、日志记录堆栈并告警。

  • 如何返回自定义业务错误码? 抛 HttpException 时把 { code, message } 作为响应体传入,过滤器读取 code 并归一化输出;前端依据 code 做细粒度处理。

自测三问

  1. NotFoundException 和裸 Error 在 HTTP 层处理结果有什么差别?
  2. 全局过滤器为什么要给响应加 path、timestamp 这类字段?
  3. 为什么生产环境不能把堆栈信息直接返回给客户端?

小结一句话

Service 抛语义异常,全局 ExceptionFilter 统一收口;500 对客户端脱敏、细节留在日志里。

延伸

  • Exception filters(官方过滤器文档)
  • Built-in HTTP exceptions(内置异常清单)
  • 下一课预告:学习自动化测试,用 Jest 做 Service 单测、用 supertest 验证接口契约。
加载本节练习…
← 上一课数据库接入入门下一课 →测试