首页 / NestJS 入门教程 / 异常处理

NestJS 入门教程

异常处理

本教程共 47 篇 · 第 14 篇 · 更新于 2026-08-09 · 约 10 分钟阅读

NestJS异常处理HttpExceptionExceptionFilter错误码全局异常

本节目标:搞懂 NestJS 怎么处理异常,学会抛异常、接异常、统一异常格式,让你的 API 错误信息干净又规范。

写代码不可能不出错。数据库连不上、用户传了非法参数、资源不存在……这些情况都会产生异常。

问题不在于异常本身,而在于你怎么处理它。

如果你什么都不做,NestJS 会帮你兜底——返回一个 500 错误和一句 “Internal server error”。但这不够友好,前端拿到这种响应根本不知道发生了什么。

这一章,我们从最简单的抛异常开始,一步步讲到全局异常过滤器,把整个异常处理链路捋清楚。

内置异常类

NestJS 提供了一个基类 HttpException,所有 HTTP 异常都从它派生。

最简单的用法:

import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll() {
    throw new HttpException('禁止访问', HttpStatus.FORBIDDEN);
  }
}

请求这个接口,返回的 JSON 长这样:

{
  "statusCode": 403,
  "message": "禁止访问"
}

HttpException 接收两个必传参数:

  • 第一个是响应体内容,可以是字符串,也可以是对象
  • 第二个是 HTTP 状态码,推荐用 HttpStatus 枚举
Note

HttpStatus 是一个枚举类型,从 @nestjs/common 导入。用枚举比直接写数字好,代码可读性更强,也不容易写错。

更常用的快捷方式

每次写 new HttpException('xxx', HttpStatus.XXX) 太啰嗦了。NestJS 提供了一堆现成的异常子类,直接拿来用就行:

import {
  Controller,
  Get,
  Param,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    if (!id) {
      throw new BadRequestException('缺少 ID 参数');
    }

    const cat = this.catsService.findOne(id);
    if (!cat) {
      throw new NotFoundException(`猫咪 ${id} 不存在`);
    }

    return cat;
  }
}

常用的内置异常类,记住这几个就够了:

异常类状态码什么时候用
BadRequestException400参数有误
UnauthorizedException401没登录
ForbiddenException403没权限
NotFoundException404资源不存在
ConflictException409数据冲突,比如重复注册
InternalServerErrorException500服务器内部错误
Tip

这些异常类都从 @nestjs/common 导入,不需要额外安装任何包。

自定义响应体

有时候默认的 { statusCode, message } 格式不够用,你想在错误响应里带更多信息。

直接把一个对象传给 HttpException 的第一个参数就行:

@Get()
findAll() {
  throw new HttpException(
    {
      status: HttpStatus.FORBIDDEN,
      error: '权限不足',
      details: { reason: '需要管理员角色' },
    },
    HttpStatus.FORBIDDEN,
  );
}

返回的 JSON 就是你传的对象:

{
  "status": 403,
  "error": "权限不足",
  "details": {
    "reason": "需要管理员角色"
  }
}
Note

当第一个参数传的是对象时,NestJS 会原样返回这个对象,不会自动加 statusCodemessage 字段。所以你需要自己定义响应结构。

错误原因 cause

有时候你抛异常是因为底层出了错,你想保留原始错误信息用于调试,但又不想把它暴露给前端。

NestJS 支持第三个参数 options,里面可以传 cause

@Get()
async findAll() {
  try {
    await this.catsService.findAll();
  } catch (error) {
    throw new HttpException(
      '获取猫咪列表失败',
      HttpStatus.INTERNAL_SERVER_ERROR,
      { cause: error },
    );
  }
}

cause 不会出现在响应里,但会保存在异常对象上,方便你在日志里追踪问题。

自定义异常类

当内置异常不够用的时候,你可以定义自己的异常类。

做法很简单,继承 HttpException 就行:

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

export class CatNotFoundException extends HttpException {
  constructor(id: string) {
    super(`猫咪 ${id} 不存在`, HttpStatus.NOT_FOUND);
  }
}

export class CatAlreadyExistsException extends HttpException {
  constructor(name: string) {
    super(
      {
        status: HttpStatus.CONFLICT,
        error: '猫咪已存在',
        catName: name,
      },
      HttpStatus.CONFLICT,
    );
  }
}

用的时候就很直观:

@Get(':id')
findOne(@Param('id') id: string) {
  const cat = this.catsService.findOne(id);
  if (!cat) {
    throw new CatNotFoundException(id);
  }
  return cat;
}
Tip

自定义异常继承自 HttpException,NestJS 会自动识别并正确处理。不需要额外注册任何东西。

建立业务错误码体系

项目大了以后,光靠 HTTP 状态码不够区分具体的业务错误。比如”用户不存在”和”用户已禁用”都是 4xx,但前端需要根据不同的错误码做不同的处理。

这时候可以建一套错误码枚举:

export enum BizErrorCode {
  USER_NOT_FOUND = 'USER_NOT_FOUND',
  USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS',
  INVALID_PASSWORD = 'INVALID_PASSWORD',
  ACCOUNT_DISABLED = 'ACCOUNT_DISABLED',
}

再定义一个业务异常基类:

import { HttpException, HttpStatus } from '@nestjs/common';
import { BizErrorCode } from './biz-error-code.enum';

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

  getErrorCode(): BizErrorCode {
    return this.errorCode;
  }
}

然后具体的业务异常就可以继承它:

import { HttpStatus } from '@nestjs/common';
import { BusinessException } from './business.exception';
import { BizErrorCode } from './biz-error-code.enum';

export class UserNotFoundException extends BusinessException {
  constructor(id: string) {
    super(
      `用户 ${id} 不存在`,
      BizErrorCode.USER_NOT_FOUND,
      HttpStatus.NOT_FOUND,
    );
  }
}

export class AccountDisabledException extends BusinessException {
  constructor(userId: string) {
    super(
      '账号已被禁用',
      BizErrorCode.ACCOUNT_DISABLED,
      HttpStatus.FORBIDDEN,
    );
  }
}

这样在 Service 层抛异常就很清晰了:

@Injectable()
export class UsersService {
  async findOne(id: string): Promise<User> {
    const user = await this.repo.findOne({ where: { id } });

    if (!user) {
      throw new UserNotFoundException(id);
    }

    if (user.disabled) {
      throw new AccountDisabledException(id);
    }

    return user;
  }
}
Note

错误码体系的好处是前端可以根据 errorCode 做精确判断,而不是去解析 message 字符串。字符串是给人类看的,错误码是给程序看的。

异常过滤器

异常过滤器是 NestJS 异常处理的核心机制。它让你完全控制异常的响应格式。

打个比方:异常过滤器就像一个”翻译官”,把各种各样的异常翻译成统一的格式返回给前端。

写一个异常过滤器

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

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    response.status(status).json({
      code: status,
      message:
        typeof exceptionResponse === 'string'
          ? exceptionResponse
          : (exceptionResponse as any).message,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

几个关键点解释一下:

@Catch(HttpException) 告诉 NestJS 这个过滤器只捕获 HttpException 类型的异常。

implements ExceptionFilter 是接口约束,要求你实现 catch 方法。

ArgumentsHost 是一个工具对象,用来获取当前请求上下文中的 requestresponse 对象。

注册过滤器

过滤器写好了,得告诉 NestJS 什么时候用它。有三种注册方式,作用范围从小到大。

方法级别——只对一个接口生效:

@Post()
@UseFilters(new HttpExceptionFilter())
create(@Body() createCatDto: CreateCatDto) {
  return this.catsService.create(createCatDto);
}

控制器级别——对整个控制器的所有接口生效:

@Controller('cats')
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

全局级别——对所有接口生效:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
Tip

推荐在 main.ts 中用 useGlobalFilters 注册全局过滤器。大多数项目只需要一个全局过滤器就够了。

用依赖注入注册全局过滤器

useGlobalFilters 有个问题:过滤器是在模块系统之外创建的,没法注入依赖。

如果你的过滤器需要注入 Logger 之类的服务,得换一种注册方式:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

APP_FILTER 这个 token 注册,过滤器就进入了模块系统,可以正常注入依赖了。

Note

虽然你在某个模块里用 APP_FILTER 注册了过滤器,但它实际上是全局生效的,不是只在那个模块里生效。

捕获所有异常

@Catch() 不传参数,就能捕获所有异常,不管是什么类型:

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

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();

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

    const message =
      exception instanceof HttpException
        ? exception.getResponse()
        : '服务器内部错误';

    const responseBody = {
      code: status,
      message,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, status);
  }
}

这里有个细节:用了 HttpAdapterHost 来获取 HTTP 适配器,而不是直接用 requestresponse

这样做的好处是代码跟具体平台解耦了。不管你用的是 Express 还是 Fastify,这套代码都能跑。

Tip

如果你的项目有可能在 Express 和 Fastify 之间切换,建议用 HttpAdapterHost 的方式。如果确定只用 Express,直接用 requestresponse 也没问题。

捕获特定类型的异常

你可以让过滤器只捕获某种特定异常:

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

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

    // 根据数据库错误码返回不同状态
    const message = this.parseDatabaseError(exception);

    response.status(400).json({
      code: 400,
      message,
      timestamp: new Date().toISOString(),
    });
  }

  private parseDatabaseError(exception: QueryFailedError): string {
    // 根据不同的数据库错误返回友好提示
    // 比如唯一约束冲突、外键约束失败等
    return '数据库操作失败';
  }
}
Note

当你同时有”捕获所有”和”捕获特定”的过滤器时,“捕获特定”的过滤器优先级更高。NestJS 会先尝试匹配具体的过滤器,匹配不到才走通用的。

继承 BaseExceptionFilter

有时候你不想从零写一个过滤器,只想在默认行为的基础上加点东西,比如记个日志。

这时候可以继承 BaseExceptionFilter

import { Catch, ArgumentsHost, Logger } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class LoggingExceptionFilter extends BaseExceptionFilter {
  private readonly logger = new Logger(LoggingExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    // 先记日志
    this.logger.error(
      `异常: ${exception instanceof Error ? exception.message : '未知错误'}`,
      exception instanceof Error ? exception.stack : undefined,
    );

    // 再交给父类处理默认的响应逻辑
    super.catch(exception, host);
  }
}
Warning

继承 BaseExceptionFilter 的全局过滤器需要注入 HttpAdapter。在 main.ts 中这样注册:

const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new LoggingExceptionFilter(httpAdapter));

异常日志

生产环境里,异常日志非常重要。你可以在过滤器里加上日志逻辑:

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

@Catch()
export class LoggingFilter implements ExceptionFilter {
  private readonly logger = new Logger('ExceptionHandler');

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();

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

    // 5xx 错误用 error 级别,4xx 用 warn 级别
    if (status >= 500) {
      this.logger.error(
        `${request.method} ${request.url} → ${status}`,
        exception instanceof Error ? exception.stack : undefined,
      );
    } else {
      this.logger.warn(
        `${request.method} ${request.url} → ${status}`,
      );
    }

    // 返回响应
    const response = ctx.getResponse();
    response.status(status).json({
      code: status,
      message: status >= 500 ? '服务器内部错误' : (exception as any)?.message || '请求失败',
      timestamp: new Date().toISOString(),
    });
  }
}
Tip

NestJS 默认不会记录 HttpException 及其子类的日志,因为它认为这是正常的业务流程(比如”用户不存在”这种 404 不需要记日志)。如果你想记录所有异常,就需要自定义过滤器。

异常处理的最佳实践

1. 在 Service 层抛异常,而不是 Controller 层

Controller 只负责接收请求和返回响应,业务逻辑(包括错误判断)放在 Service 里:

// Service 层
@Injectable()
export class CatsService {
  async findOne(id: string): Promise<Cat> {
    const cat = await this.repo.findOne({ where: { id } });
    if (!cat) {
      throw new CatNotFoundException(id);
    }
    return cat;
  }
}

// Controller 层 — 干干净净
@Get(':id')
findOne(@Param('id') id: string) {
  return this.catsService.findOne(id);
}

2. 统一异常响应格式

前端最怕的是每个接口返回的错误格式都不一样。用一个全局异常过滤器统一格式:

{
  "code": 404,
  "message": "猫咪 123 不存在",
  "errorCode": "CAT_NOT_FOUND",
  "timestamp": "2026-08-09T12:00:00.000Z",
  "path": "/cats/123"
}

3. 不要暴露敏感信息

500 错误不要返回数据库错误详情、堆栈信息之类的东西。这些信息只记在日志里,返回给前端的永远是一句简洁的提示。

4. 善用 cause 保留错误链

try {
  await this.db.query('...');
} catch (error) {
  throw new HttpException(
    '数据查询失败',
    HttpStatus.INTERNAL_SERVER_ERROR,
    { cause: error },  // 保留原始错误,方便排查
  );
}
Note

cause 是 Error 对象的标准属性(ES2022 引入),NestJS 的 HttpException 也支持它。cause 不会出现在 HTTP 响应里,但可以通过 exception.cause 在过滤器中获取到。

本章小结

这一章讲了 NestJS 异常处理的完整链路:

  • HttpException 是异常的基类,内置了一堆常用子类
  • 自定义异常继承 HttpException,代码更清晰
  • 异常过滤器让你控制错误的响应格式
  • 过滤器可以绑定到方法、控制器、或者全局
  • APP_FILTER token 注册全局过滤器,支持依赖注入
  • 日志和敏感信息保护是生产环境的必备

异常处理做得好,API 的可用性直接上一个台阶。下一章我们聊中间件,看看怎么在请求到达控制器之前做一些通用处理。