Node.js 后端三层架构中的错误处理最佳实践
引言
在构建可靠的后端系统时,错误处理是提高系统稳定性和可维护性的关键环节。本文将详细分析基于仓库(Repository)、服务(Service)和控制器(Controller)三层架构中的错误处理策略,通过实际代码示例展示如何实现清晰、一致的错误处理流程。
三层架构中的错误处理责任
仓库层(Repository)
主要职责:
捕获原始数据库错误
记录详细的技术错误信息
转换为更具体的技术错误
通过抛出异常向上传递
示例实现:
public static async getTimeline(userId: number): Promise<TimelineDB[]> {
try {
const [rows] = await pool.query<RowDataPacket[]>(
"SELECT * FROM timeline WHERE userId = ?",
[userId]
);
return rows.map((row: TimelineDB) => ({
id: row.id,
userId: row.userId,
content: row.content,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}));
} catch (err: any) {
console.error("数据库查询错误:", err);
throw new Error(`获取时间线数据失败: ${err.message}`);
}
}
服务层(Service)
主要职责:
捕获仓库层错误
执行业务逻辑验证
转换为业务领域错误
添加业务上下文信息
通过抛出异常向上传递
示例实现:
public static async getTimelineData(userId: number): Promise<TimelineDB[]> {
try {
const timeline = await TimelineRepository.getTimeline(userId);
if (!timeline || timeline.length === 0) {
throw new Error("未找到时间线数据");
}
return timeline;
} catch (err: any) {
if (err.message.includes("获取时间线数据失败")) {
throw new Error(`获取时间线失败: ${err.message}`);
}
throw err;
}
}
控制器层(Controller)
主要职责:
捕获所有下层异常
转换为 HTTP 响应
设置合适的状态码
返回统一格式的错误信息给客户端
示例实现:
public static async getTimelineData(ctx: Context): Promise<void> {
try {
const userId = ctx.state.user.id;
const timeline = await TimeLineService.getTimelineData(userId);
ctx.body = success(timeline, "获取时间线数据成功");
} catch (err: any) {
ctx.body = error(err.message, 500);
}
}
错误处理的核心原则
1. 层级分明的错误转换
每一层应该对接收到的错误进行适当转换,使其符合当前层的抽象级别:
仓库层:数据库错误 → 数据访问错误
服务层:数据访问错误 → 业务领域错误
控制器层:业务领域错误 → HTTP 响应错误
2. 错误信息逐层增强
错误在向上传递过程中,应不断增强其语义和上下文信息:
数据库错误: "ER_BAD_FIELD_ERROR"
↓
仓库层错误: "获取时间线数据失败: 字段'content'不存在"
↓
服务层错误: "获取时间线失败: 获取时间线数据失败: 字段'content'不存在"
↓
控制器响应: { success: false, message: "获取时间线失败...", code: 500 }
3. 统一的错误处理模式
在每一层中保持一致的错误处理模式:
使用 try-catch 捕获异常
区分不同类型的错误
记录合适级别的日志
重新抛出增强后的错误
实现最佳实践
仓库层错误处理
try {
// 数据库操作
} catch (err: any) {
// 1. 记录详细的技术错误
console.error("数据库操作错误:", err);
// 2. 区分错误类型
if (err.code === "ER_DUP_ENTRY") {
throw new Error(`数据已存在: ${err.message}`);
}
// 3. 提供清晰的技术上下文
throw new Error(`数据库操作失败: ${err.message}`);
}
服务层错误处理
try {
// 调用仓库方法
const result = await repository.method();
// 业务逻辑验证
if (!isValid(result)) {
throw new Error("业务规则验证失败");
}
return result;
} catch (err: any) {
// 1. 区分错误来源
if (err.message.includes("数据库操作失败")) {
// 2. 为技术错误添加业务上下文
throw new Error(`业务操作失败: ${err.message}`);
}
// 3. 原样传递业务错误
throw err;
}
控制器层错误处理
try {
// 参数验证
if (!isValidRequest(ctx.request.body)) {
return (ctx.body = error("请求参数无效", 400));
}
// 调用服务
const result = await service.method();
return (ctx.body = success(result, "操作成功"));
} catch (err: any) {
// 1. 确定适当的HTTP状态码
const statusCode = determineStatusCode(err);
// 2. 返回统一格式的响应
ctx.body = error(err.message, statusCode);
}
错误处理中的日志级别
不同层应使用适当的日志级别记录错误:
仓库层:
error
- 记录详细的技术错误和堆栈服务层:
warn
- 记录业务处理异常控制器层:
info
- 记录请求处理结果
性能考虑
虽然 try-catch 结构有轻微的性能开销,但在现代 JavaScript 引擎中已经高度优化。相比错误处理带来的系统稳定性和可维护性提升,这种性能影响可以忽略不计:
错误处理代码路径很少执行
数据库和网络操作的延迟远大于 try-catch 的开销
代码的可读性和一致性比微小性能优化更重要
结论
一个良好的错误处理系统应该像过滤器一样,在错误向上传递的过程中,不断过滤技术细节,增强业务语义,最终向客户端返回清晰、友好且安全的错误信息。
通过在仓库、服务和控制器三层中实施一致的错误处理策略,可以显著提高系统的可靠性、可维护性和用户体验。无论是简单应用还是复杂系统,这些原则都能帮助开发团队构建更加健壮的后端服务。
最重要的是,好的错误处理不仅仅是捕获异常,而是将异常转化为有价值的信息,帮助开发者快速定位问题,帮助用户理解发生了什么。