异常处理
本教程共 47 篇 · 第 14 篇 · 更新于 2026-08-09 · 约 10 分钟阅读
本节目标:搞懂 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;
}
}
常用的内置异常类,记住这几个就够了:
| 异常类 | 状态码 | 什么时候用 |
|---|---|---|
BadRequestException | 400 | 参数有误 |
UnauthorizedException | 401 | 没登录 |
ForbiddenException | 403 | 没权限 |
NotFoundException | 404 | 资源不存在 |
ConflictException | 409 | 数据冲突,比如重复注册 |
InternalServerErrorException | 500 | 服务器内部错误 |
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 会原样返回这个对象,不会自动加
statusCode和message字段。所以你需要自己定义响应结构。
错误原因 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 是一个工具对象,用来获取当前请求上下文中的 request 和 response 对象。
注册过滤器
过滤器写好了,得告诉 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 适配器,而不是直接用 request 和 response。
这样做的好处是代码跟具体平台解耦了。不管你用的是 Express 还是 Fastify,这套代码都能跑。
Tip如果你的项目有可能在 Express 和 Fastify 之间切换,建议用
HttpAdapterHost的方式。如果确定只用 Express,直接用request和response也没问题。
捕获特定类型的异常
你可以让过滤器只捕获某种特定异常:
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(),
});
}
}
TipNestJS 默认不会记录
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_FILTERtoken 注册全局过滤器,支持依赖注入 - 日志和敏感信息保护是生产环境的必备
异常处理做得好,API 的可用性直接上一个台阶。下一章我们聊中间件,看看怎么在请求到达控制器之前做一些通用处理。