如何在Express控制器/服务中处理错误?架构优化咨询
方案分析与优化建议
你的核心顾虑非常合理:当前服务层抛出带有statusCode的NotFound错误,确实把HTTP协议相关细节耦合进了本应专注业务逻辑的服务层,违反了单一职责原则。下面分两种思路给出具体分析和优化方案:
一、当前方案的问题
服务层的核心职责是处理业务逻辑与数据交互,它不应该感知HTTP协议的存在——404是HTTP的状态码,属于API接口的表现层范畴,硬编码在服务层的错误类里会导致:
- 服务层无法跨场景复用(比如后续用该服务给CLI工具或内部消息队列提供数据时,
statusCode完全无用); - 后续修改HTTP响应规则时,需要改动服务层代码,破坏开闭原则。
二、优化方案1:服务层抛业务错误,中间件做HTTP映射(推荐)
把业务错误和HTTP细节彻底解耦,服务层只抛出描述业务事实的错误,由专门的中间件负责将业务错误映射为HTTP响应。
1. 定义纯业务错误类
去掉HTTP相关的statusCode,用错误名称标识业务类型:
class UserNotFoundError extends Error { constructor(message = "用户不存在") { super(message); this.name = "UserNotFoundError"; // 用name区分不同业务错误 } }
2. 修改服务层代码
只关注业务逻辑,抛出业务错误,去掉不必要的try-catch(直接让错误向上传递即可):
findOne: async (userId) => { const user = await db.users.findByPk(userId); if (!user) { throw new UserNotFoundError(); } return user; };
3. 新增错误映射中间件
在errorResponder之前添加一个中间件,负责把业务错误转换成带HTTP状态码的错误:
const errorMapper = (error, req, res, next) => { switch (error.name) { case "UserNotFoundError": error.statusCode = 404; break; case "InvalidInputError": // 比如参数验证错误 error.statusCode = 400; break; // 其他业务错误类型可以在这里扩展 default: // 未匹配到的错误默认按500处理 error.statusCode = error.statusCode || 500; } next(error); };
4. 保持控制器和原错误中间件不变
控制器只负责参数传递和调用服务,错误依然通过next(error)传递给中间件链。
这种方式的优势:
- 服务层完全聚焦业务,符合单一职责;
- HTTP相关逻辑集中在中间件,便于统一维护(比如要修改某个业务错误对应的HTTP状态,只改中间件即可);
- 业务错误可以在非HTTP场景复用。
三、优化方案2:控制器做空值检查
如果坚持让服务层完全不抛错误,只返回数据或null,可以让控制器负责判断结果并抛出HTTP错误:
修改服务层
findOne: async (userId) => { return await db.users.findByPk(userId); };
修改控制器
findUser: async (req,res,next) => { const userId = req.params.userId; try { const user = await UserService.findOne(userId); if (!user) { throw new NotFound("User not found"); // 这里抛出HTTP相关错误 } res.status(200).send(user); } catch (error) { return next(error) } }
这种方式的优缺点:
- 优点:服务层完全“纯”,只做数据查询;
- 缺点:控制器会充斥大量重复的空值/错误检查逻辑,如果多个控制器调用同一个服务方法,会产生冗余代码;业务错误的语义不够明确(
null可能代表多种情况,不如自定义业务错误清晰)。
总结
更推荐方案1:通过业务错误+中间件映射的方式,既保证服务层的单一职责,又避免控制器的冗余代码,同时让HTTP相关逻辑集中可控。
内容的提问来源于stack exchange,提问作者Andreas
相关产品推荐
相关产品推荐

