异常处理与日志记录
在后端开发中,异常处理和日志记录是保障系统健壮性、可维护性和可观测性的关键环节。NestJS 提供了完善的异常处理体系,并支持灵活的日志集成,帮助开发者优雅地管理错误响应和系统日志。
NestJS 异常体系概览
NestJS 基于 HTTP 异常(HttpException)设计了一套统一的异常处理机制。所有未被捕获的异常,最终都会被全局异常过滤器(Exception Filter)处理,返回结构化的错误响应。
- 内置异常类:如
BadRequestException、NotFoundException、UnauthorizedException等,继承自HttpException,可直接在业务代码中抛出。 - 自定义异常:可继承
HttpException或实现ExceptionFilter,满足特殊业务需求。 - 全局异常过滤器:通过
@Catch()装饰器实现,统一处理所有未捕获的异常。 - 拦截器:可用于统一响应格式、日志记录等。
使用内置异常类
NestJS 提供了丰富的内置异常类,推荐在业务逻辑中直接抛出,框架会自动捕获并返回标准 HTTP 响应。
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
async getUser(@Param('id') id: number) {
const user = await this.userService.findById(id);
if (!user) {
throw new NotFoundException('用户不存在');
}
return user;
}
}
- 这样抛出的异常会自动被 NestJS 处理,返回 404 状态码和标准错误结构。
自定义异常过滤器(Exception Filter)
当需要统一错误响应格式、记录日志或处理特殊异常时,可以自定义异常过滤器。
// filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } 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>();
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException ? exception.getResponse() : exception;
// 日志记录
this.logger.error(`HTTP ${status} ${request.method} ${request.url} - ${JSON.stringify(message)}`);
response.status(status).json({
code: status,
message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
- 通过
@Catch()装饰器可捕获所有异常,也可指定特定异常类型。 - 统一返回自定义的错误结构,便于前端统一处理。
- 在过滤器中通过
Logger记录异常日志,便于后期排查。
注册全局异常过滤器
在 main.ts 中注册全局异常过滤器:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
bootstrap();
自定义异常类
如需自定义业务异常,可继承 HttpException,实现更细致的错误码和信息。
import { HttpException, HttpStatus } from '@nestjs/common';
export class BusinessException extends HttpException {
constructor(message: string, code = 10001) {
super({ code, message }, HttpStatus.BAD_REQUEST);
}
}
// 使用
throw new BusinessException('手机号已注册', 20001);
全局拦截器统一响应格式
拦截器可用于统一 API 响应结构,无论成功还是失败都返回一致格式,便于前端处理。
// interceptors/response.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
private readonly logger = new Logger(ResponseInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
return next.handle().pipe(
map(data => ({ code: 0, data, message: 'success' })),
tap(() => {
this.logger.log(`Response: [${req.method}] ${req.url}`);
}),
);
}
}
在 main.ts 注册全局拦截器:
import { ResponseInterceptor } from './interceptors/response.interceptor';
app.useGlobalInterceptors(new ResponseInterceptor());
日志记录最佳实践
日志是后端系统定位问题、追踪请求、监控健康状况的重要手段。NestJS 内置了简单的 Logger,也支持集成 winston、pino 等专业日志库。
1. 日志分级与输出
- Logger 支持
log、error、warn、debug、verbose等分级,建议根据场景选择合适的级别。 - 生产环境建议将日志输出到文件或集中式日志平台(如 ELK、Loki、Sentry 等)。
2. 在中间件记录请求日志
可以通过中间件统一记录每个请求的访问日志:
// middlewares/logger.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl } = req;
res.on('finish', () => {
const { statusCode } = res;
this.logger.log(`${method} ${originalUrl} ${statusCode}`);
});
next();
}
}
在模块中注册中间件:
// app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({ /* ... */ })
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}
3. 日志与异常结合
- 在异常过滤器中记录错误日志,便于追踪异常来源。
- 在拦截器或中间件中记录请求和响应日志,便于分析接口性能和调用链。
- 日志内容建议包含请求方法、路径、状态码、耗时、错误堆栈等关键信息。
4. 日志库扩展
- 对于大型项目,建议集成 winston、pino 等日志库,支持日志格式化、异步写入、日志切割、远程推送等高级功能。
- 可通过自定义 LoggerService 实现日志统一管理。
实战场景:统一 API 错误返回格式与日志追踪
在实际项目中,推荐统一所有接口的错误返回结构,并结合日志记录每一次异常和请求。例如:
{
"code": 404,
"message": "用户不存在",
"path": "/user/123",
"timestamp": "2024-06-01T12:00:00.000Z"
}
这样前端可以根据 code 字段统一处理异常,后端可以通过日志快速定位问题,提升用户体验和系统可维护性。
最佳实践与常见坑点
- 优先使用内置异常类,如 404、400、401 等,避免手写状态码。
- 全局异常过滤器要注册在 main.ts,否则无法捕获所有异常。
- 自定义异常要继承 HttpException,保证结构一致。
- 拦截器和过滤器要分工明确,避免重复处理。
- 错误信息不要暴露敏感数据,如数据库错误、堆栈信息等。
- 日志记录建议在过滤器、中间件、拦截器中实现,便于排查问题。
- 生产环境建议集成专业日志库,支持日志分级、持久化和远程推送。
合理的异常处理和日志记录是高质量后端的标志,建议多参考 NestJS 官方文档 和实际项目最佳实践。