- 文档
- 教程
- 后端
【免费下载链接】nodebestpractices
✅ The Node.js best practices list (July 2026)
在 Node.js 应用中,错误并非一概而论:操作性错误(operational errors)描述的是"发生了什么、影响是什么"——例如因连接问题导致某个 HTTP 服务查询失败;而程序员错误(programmer errors)则是"不知道从哪来、也不知道为什么"的缺陷——例如试图读取一个未定义的值,或是一个持续泄漏内存的数据库连接池。学会区分这两类错误,是最大限度减少应用停机时间(downtime)、避免诡异 bug 的第一道防线。本文以开源仓库 nodebestpractices 中《区分操作性错误与程序员错误》最佳实践为主体,结合仓库内错误处理系列文档的源码级示例,带你建立一套"可标记、可判断、可恢复"的完整错误处理体系。
两类错误的本质区别
先给这两类错误下一个精确的定义:
| 维度 | 操作性错误(Operational Error) | 程序员错误(Programmer Error) |
|---|---|---|
| 发生原因 | 外部环境或运行时状况,例如网络抖动、数据库连接失败、用户输入非法 | 代码本身的缺陷,例如读取未定义值、逻辑分支错误、内存泄漏 |
| 是否可以预测 | 属于可预期的运行时状况,可被妥善处理 | 属于 bug,出现时应用状态可能已经损坏 |
| 处理方式 | 通常记录日志即可,无需中断服务 | 应立即崩溃并优雅重启,避免带病运行 |
| 典型例子 | HTTP 服务查询因连接问题失败 | 读取 undefined 值、连接池泄漏内存 |
操作性错误相对容易处理——记录日志往往就足够了。而程序员错误一旦出现,应用可能已处于不一致(inconsistent)状态,此时没有比重启更优的选择。这正是本实践的核心论点:把"可恢复的错误"与"必须重启的错误"在源头区分开,是构建健壮 Node.js 服务的基石。
在代码中标记操作性错误
要让集中式错误处理器能区分两类错误,首先需要在抛出错误时就打上标记。仓库文档给出的做法是给错误对象附加isOperational属性:
// marking an error object as operational const myError = new Error("How can I add new product when no value provided?"); myError.isOperational = true;更推荐的做法是使用集中式错误工厂(可参见同系列实践 Use only the built-in Error object),让每个应用级错误都继承自内置Error并携带结构化信息:
class AppError { constructor (commonType, description, isOperational) { Error.call(this); Error.captureStackTrace(this); this.commonType = commonType; this.description = description; this.isOperational = isOperational; } }; throw new AppError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);在 TypeScript 中,AppError通过extends Error实现同样的语义,并借助Object.setPrototypeOf(this, new.target.prototype)恢复原型链,保证instanceof判断正确:
// some centralized error factory (see other examples at the bullet "Use only the built-in Error object") export class AppError extends Error { public readonly commonType: string; public readonly isOperational: boolean; constructor(commonType: string, description: string, isOperational: boolean) { super(description); Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain this.commonType = commonType; this.isOperational = isOperational; Error.captureStackTrace(this); } } // marking an error object as operational (true) throw new AppError(errorManagement.commonErrors.InvalidInput, 'Describe here what happened', true);使用内置Error而非字符串或自定义裸对象有显著收益:它保留stackTrace等关键信息、与第三方库保持统一契约,并且只扩展一次AppError即可覆盖所有应用级错误,无需为DbError、HttpError等逐一扩展。参考同系列实践 Use only the built-in Error object 中的反模式示例,throw ('How can I add new product when no value provided?')这种抛字符串的方式会丢失全部堆栈信息,应坚决避免。
集中式处理器:判断"可信错误"并决定是否崩溃
仅打标记还不够,还需要一个集中式错误处理器来统一消费这些标记。参考同系列实践 Exit the process gracefully when a stranger comes to town,错误处理的核心逻辑被封装在errorHandler中,它负责三件事:记录日志、通知运维(如发邮件)、判断是否为"可信错误"(trusted error,即操作性错误):
process.on('uncaughtException', (error) => { errorManagement.handler.handleError(error); if(!errorManagement.handler.isTrustedError(error)) process.exit(1) }); // centralized error handler encapsulates error-handling related logic function errorHandler() { this.handleError = (error) => { return logger.logError(error) .then(sendMailToAdminIfCritical) .then(saveInOpsQueueIfCritical) .then(determineIfOperationalError); } this.isTrustedError = (error) => { return error.isOperational; } }这段代码揭示了完整决策链路:
- 未捕获异常到达
uncaughtException事件; - 处理器先执行日志、告警、运维队列等记录动作;
- 再通过
isTrustedError(error)检查error.isOperational标记; - 若为可信的操作性错误 → 仅记录,不退出进程;若为不可信的程序员错误 →
process.exit(1)退出,交由 PM2、Forever 等守护进程(restarter)自动重启。
TypeScript 版本的isTrustedError判断更为严格——只有AppError实例才被认可为可判定错误,其余一律视为不可信:
export class AppError extends Error { public readonly isOperational: boolean; constructor(description: string, isOperational: boolean) { super(description); Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain this.isOperational = isOperational; Error.captureStackTrace(this); } } class ErrorHandler { public async handleError(err: Error): Promise<void> { await logger.logError(err); await sendMailToAdminIfCritical(); await saveInOpsQueueIfCritical(); await determineIfOperationalError(); }; public isTrustedError(error: Error) { if (error instanceof AppError) { return error.isOperational; } return false; } } export const handler = new ErrorHandler();仓库同系列实践 Handle errors centrally. Not within middlewares 进一步强调:不要把所有错误处理逻辑塞进 Express 错误中间件,中间件只负责"捕获并转发"给集中处理器,否则定时任务(Cron job)、消息队列订阅者、未捕获异常等场景的错误将无人处理。典型的错误流转链路如下:
错误处理参与者与流转流程
// DAL layer, we don't handle errors here DB.addDocument(newCustomer, (error, result) => { if (error) throw new Error('Great error explanation comes here', other useful parameters) }); // API route code, we catch both sync and async errors and forward to the middleware try { customerService.addNew(req.body).then((result) => { res.status(200).json(result); }).catch((error) => { next(error) }); } catch (error) { next(error); } // Error handling middleware, we delegate the handling to the centralized error handler app.use(async (err, req, res, next) => { await errorHandler.handleError(err, res);//The error handler will send a response }); process.on("uncaughtException", error => { errorHandler.handleError(error); }); process.on("unhandledRejection", (reason) => { errorHandler.handleError(reason); });注意最后两行:unhandledRejection事件同样被接入集中式处理器。结合同系列实践 Catch unhandled promise rejections,现代 Node.js/Express 代码大量运行在 Promise 中,开发者一旦遗漏.catch,错误就会"消失"而非进入uncaughtException。正确的兜底方式是:让unhandledRejection处理器throw reason,将其转为uncaughtException,再由上述链路统一判断与决策:
process.on('unhandledRejection', (reason, p) => { // I just caught an unhandled promise rejection, // since we already have fallback handler for unhandled errors (see below), // let throw and let him handle that throw reason; }); process.on('uncaughtException', (error) => { // I just received an error that was never handled, time to handle it and then decide whether a restart is needed errorManagement.handler.handleError(error); if (!errorManagement.handler.isTrustedError(error)) process.exit(1); });为什么程序员错误必须"立即崩溃"
面对程序员错误,社区的主流共识是"立即崩溃"。Joyent 博客(在 "Node.js error handling" 关键词下排名第一)给出了最直接的论证:
……程序员错误最好的恢复方式就是立即崩溃。你应当使用能在崩溃时自动重启程序的 restarter 工具来运行程序。有了 restarter 之后,面对瞬时性(transient)的程序员错误,崩溃是恢复可靠服务最快的方式……
这条建议背后的原理在 Node.js 官方文档中阐述得更为透彻——由于 JavaScript 中throw的工作方式使然,几乎不存在一种安全的方式能"从断点处接着干",而不泄漏引用或制造某种未定义的脆弱状态(undefined brittle state):
……处理被抛出的错误最安全的方式是关闭进程。当然,在普通 Web 服务器中可能开着大量连接,因为某个人的请求触发错误就粗暴关闭所有连接并不合理。更好的做法是:向触发错误的请求返回错误响应,让其他请求在正常时间内完成,并在该 worker 中停止监听新请求……
debugable.com 博客(在 "Node.js uncaught exception" 关键词下排名第三)从状态一致性角度补充了警告:
……除非你真的清楚自己在做什么,否则在收到
uncaughtException事件后应当执行一次优雅重启。否则,你的应用状态或第三方库的状态可能变得不一致,从而引发各种疯狂(crazy)的 bug……
需要特别强调的是:这里的"崩溃"并非粗暴退出,而是优雅重启。以 Node.js 官方文档的建议为蓝本,Web 服务器场景下的合理顺序是:对触发错误的请求返回 500 错误响应 → 停止监听新请求 → 让进行中的请求自然完成 → 由守护进程(如 PM2)以干净状态重新拉起进程。仓库 sections/docker/graceful-shutdown.md 与 sections/errorhandling/shuttingtheprocess.md 均围绕这一"信任边界 + 优雅退出"理念展开。
错误处理的三种思想流派
JS Recipes 博客将业界对错误处理的态度归纳为三种流派,理解了它们,你就能明白本文所推荐方案在整个光谱中的位置:
……错误处理主要有三种思路:
- 让应用崩溃并重启它。
- 处理所有可能的错误,绝不崩溃。
- 介于两者之间的平衡方案。
第一种流派依赖 restarter 快速恢复,但对操作性错误也"一刀切"重启,会造成不必要的停机;第二种流派试图"永不崩溃",却往往掩盖了程序员错误导致的损坏状态,反而把问题放大成更难排查的诡异 bug。nodebestpractices 推荐的正是第三种平衡方案:通过isOperational标记 + 集中式处理器 +isTrustedError判断,把"该记录的记录、该崩溃的崩溃"精确分流,既把停机时间压到最低,又避免带病运行。
让错误处理闭环:fail-fast 与错误流测试
要让上述体系真正可信,还需两道防线兜底:
第一道防线是 fail-fast(快速失败)。参考同系列实践 Fail fast, validate arguments using a dedicated library,在函数入口用 Joi、Validator 等库对参数先行断言,可以把大量潜在的"程序员错误"拦截在源头,而不是等到运行中途以诡异形态爆发:
const memberSchema = Joi.object().keys({ password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/), birthyear: Joi.number().integer().min(1900).max(2013), email: Joi.string().email() }); function addNewMember(newMember) { // assertions come first Joi.assert(newMember, memberSchema); //throws if validation fails // other logic here }第二道防线是错误流测试。参考同系列实践 Test error flows using your favorite test framework,只测"快乐路径"等于没测——必须验证异常路径确实被正确处理。仓库文档给出了用 Mocha & Chai 断言异常抛出的示例,以及用 sinon 桩模拟AppError、断言 API 返回 500 且日志记录完整字段的集成测试:
test("When exception is throw during request, Then logger reports the mandatory fields", async () => { //Arrange const orderToAdd = { userId: 1, productId: 2, }; sinon .stub(OrderRepository.prototype, "addOrder") .rejects(new AppError("saving-failed", "Order could not be saved", 500)); const loggerDouble = sinon.stub(logger, "error"); //Act const receivedResponse = await axiosAPIClient.post("/order", orderToAdd); //Assert expect(receivedResponse.status).toBe(500); expect(loggerDouble.lastCall.firstArg).toMatchObject({ name: "saving-failed", status: 500, stack: expect.any(String), message: expect.any(String), }); });小结:一套可落地的错误处理决策链
将仓库 nodebestpractices 错误处理系列实践串联起来,你可以得到这样一套完整的决策链:
- 源头标记:所有应用级错误统一继承内置
Error(见 useonlythebuiltinerror.md),并用isOperational标记操作性错误; - 集中判断:由集中式
ErrorHandler统一执行日志、告警、监控,并通过isTrustedError判断是否为可信错误(见 shuttingtheprocess.md); - 分类处置:操作性错误 → 记录后继续服务;程序员错误 →
process.exit(1)交给 restarter 优雅重启(见 centralizedhandling.md); - 兜底捕获:用
unhandledRejection兜住被遗忘的 Promise 拒绝,统一转入uncaughtException链路(见 catchunhandledpromiserejection.md); - 前置防御:参数校验 fail-fast + 错误流测试,把程序员错误挡在源头(见 failfast.md 与 testingerrorflows.md)。
区分操作性错误与程序员错误,不是一次理论辨析,而是贯穿"标记 → 捕获 → 判断 → 处置 → 防御"全流程的工程决策。参照本文给出的代码骨架,你便能让每一次错误都在正确的位置得到正确的对待:该恢复的绝不误杀,该重启的绝不姑息。
- 文档
- 教程
- 后端
【免费下载链接】nodebestpractices
✅ The Node.js best practices list (July 2026)
相关推荐
Node.js 错误处理实战:区分操作型错误与程序型错误(nodebestpractices 实践指南)
Node.js 错误处理实战:区分操作型错误与程序型错误(nodebestpractices 实践指南) 导读 在 Node.js 服务端开发中,错误处理方式直
文档教程后端区分操作错误与编程错误:Node.js 错误处理体系的根基(nodebestpractices 实践指南)
区分操作错误与编程错误:Node.js 错误处理体系的根基(nodebestpractices 实践指南) 操作错误(Operational Errors)与编
文档教程后端Node.js 错误处理之分清操作错误与程序员错误:分类、标记与优雅重启实战(nodebestpractices)
Node.js 错误处理之分清操作错误与程序员错误:分类、标记与优雅重启实战(nodebestpractices) 导读 在 Node.js 后端应用的日常开发
文档教程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考