为何在Express应用中更推荐使用错误处理中间件而非在错误类中直接处理响应?
先聊聊你的场景:你在Express里用MVC模式开发后台,就像这个管理员登录控制器的代码一样——先做参数校验,调用登录服务类验证用户身份,成功就写入Session并跳转到后台;不管是校验失败、服务层/DAO抛出异常,都会进入catch块,通过next(e)把错误交给统一的错误处理中间件,再由中间件根据错误类型返回对应的响应。
exports.login = async (req, res, next) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new ValidationError(422,'validation error', errors); throw error; } const loginService = new LoginService(AdminDAO, req.body.username, req.body.password); const userId = await loginService.authenticateUser(); req.session.adminUserId = userId; // 只有服务类/DAO未抛错时才会执行 res.redirect("/admin"); } catch (e) { return next(e); } };
你后来想到另一种思路:给每个自定义错误类加一个handleError(res)方法,直接在catch块里调用e.handleError(res)来返回响应,不用再走错误处理中间件。但你疑惑的是,为什么业界更推荐用中间件的方式?
下面就来拆解错误处理中间件的核心优势:
1. 遵循单一职责,职责边界更清晰
自定义错误类的核心职责应该是定义错误本身的属性与类型——比如错误码、错误信息、关联的校验结果这些“错误是什么”的信息;而如何给客户端返回响应属于HTTP请求处理层的逻辑,不该和错误本身耦合。
如果把handleError(res)写在错误类里,相当于让错误类既要管“是什么错”,又要管“怎么给用户说这个错”,违反了单一职责原则。举个实际的坑:要是你后来想把这个错误类复用在非HTTP场景(比如定时任务、CLI工具),那里面依赖res对象的handleError方法就完全没用,甚至会直接报错——这就把错误类的使用场景死死绑定在Web请求里了。
2. 统一管控逻辑,降低维护成本
用错误处理中间件的话,所有错误的响应逻辑都集中在一个(或一组)中间件里:
- 你不用在每个控制器的catch块里写重复的响应代码,也不用给每个错误类都实现一遍
handleError; - 后续要调整响应规则时,比如统一给所有错误加请求ID日志、把响应格式从HTML改成JSON、新增错误类型的响应逻辑,只要修改中间件就行,不用去每个错误类里挨个调整。
就像你提到的,中间件可以用策略模式(封装不同错误的处理策略,通过多态方法返回响应)来符合开闭原则——这种扩展也是在统一的地方做,比散在各个错误类里要清晰得多。
3. 贴合Express框架的设计规范
Express的核心就是中间件链模型:请求进来后经过一串中间件处理,最后要么返回响应,要么抛错交给错误处理中间件。用中间件处理错误是框架的标准玩法,其他维护你代码的开发者一看就懂,不用额外去理解你自定义的handleError逻辑。
而且,要是错误在非控制器的地方抛出(比如其他中间件、异步任务里),直接调用next(e)就能让中间件接住;但如果用错误类自己的handleError,在这些场景里你可能拿不到res对象,或者要手动传递,反而麻烦。
4. 扩展性更强,适配多场景需求
比如你的后台既要给浏览器返回错误页面,又要给移动端APP返回JSON接口——用中间件的话,你可以在中间件里根据请求的Accept头、路由前缀动态调整响应格式;但如果把响应逻辑写在错误类里,你就得给错误类加一堆判断,或者写多个版本的错误类,反而会让代码更臃肿。
总结
当然不是说给错误类加handleError完全不可行,只是在Express这种基于中间件的框架里,用统一的错误处理中间件能让代码的职责更清晰、维护更方便,也更符合框架的设计思路,长期来看扩展性和可维护性都更强。
备注:内容来源于stack exchange,提问作者Kevin Greetham

