以下是对该主题的许多不同来源的总结和整理,包括代码示例和所选博客文章的引用。最佳实践的完整列表can be found here
Node.JS 错误处理的最佳实践
Number1:使用 Promise 进行异步错误处理
TL;DR: 以回调方式处理异步错误可能是通往地狱的最快方式(也就是末日金字塔)。你可以给你的代码最好的礼物是使用一个有信誉的 Promise 库,它提供了很多紧凑和熟悉的代码语法,比如 try-catch
否则: Node.JS 回调样式,function(err, response),由于错误处理与随意代码、过度嵌套和笨拙的编码混合在一起,是一种很有前途的不可维护代码的方法模式
代码示例 - 不错
doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);
代码示例反模式 - 回调样式错误处理
getData(someParameter, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(a, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(b, function(c){
getMoreData(d, function(e){
...
});
});
});
});
});
博客引用:“我们的承诺有问题”
(来自博客 pouchdb,关键词“Node Promises”排名第 11)
"...事实上,回调做了一些更险恶的事情:它们剥夺了我们的堆栈,这是我们在编程语言中通常认为理所当然的事情。编写没有堆栈的代码很像开车没有刹车踏板:你不会意识到你多么需要它,直到你伸手去拿它却不在那里。promise 的全部意义在于把我们在异步时丢失的语言基础知识还给我们:return,throw,和堆栈。但是您必须知道如何正确使用 Promise 才能利用它们。"
Number2:仅使用内置的 Error 对象
TL;DR: 以字符串或自定义类型抛出错误的代码很常见——这使错误处理逻辑和模块之间的互操作性变得复杂。无论您拒绝承诺、抛出异常还是发出错误 - 使用 Node.JS 内置的 Error 对象都可以提高一致性并防止错误信息丢失
否则:在执行某些模块时,不确定会返回哪种类型的错误——这使得推理和处理即将到来的异常变得更加困难。更值得一提的是,使用自定义类型来描述错误可能会导致关键错误信息(如堆栈跟踪)丢失!
代码示例 - 正确操作
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
代码示例反模式
//throwing a String lacks any stack trace information and other important properties
if(!productToAdd)
throw ("How can I add new product when no value provided?");
博客引用:“字符串不是错误”
(来自博客devthought,关键词“Node.JS错误对象”排名第6)
"...传递字符串而不是错误会导致模块之间的互操作性降低。它会破坏与可能正在执行 instanceof 错误检查或想了解有关错误的更多信息的 API 的合同。错误正如我们将看到的,对象在现代 JavaScript 引擎中除了保存传递给构造函数的消息外,还具有非常有趣的属性。”
编号 3:区分操作错误和程序员错误
TL;DR: 操作错误(例如 API 接收到无效输入)是指已知情况,其中错误影响已被完全理解并且可以经过深思熟虑进行处理。另一方面,程序员错误(例如,试图读取未定义的变量)是指未知的代码故障,要求优雅地重新启动应用程序
否则:当出现错误时,您可能总是会重新启动应用程序,但为什么要让大约 5000 名在线用户因为一个小错误和预测错误(操作错误)而失望呢?相反的情况也不理想——在发生未知问题(程序员错误)时保持应用程序正常运行可能会导致无法预料的行为。区分两者允许机智地采取行动,并根据给定的上下文应用平衡的方法
代码示例 - 正确操作
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
代码示例 - 将错误标记为可操作(可信)
//marking an error object as operational
var myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;
//or if you're using some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
function appError(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);
//error handling code within middleware
process.on('uncaughtException', function(error) {
if(!error.isOperational)
process.exit(1);
});
博客引用:“否则你会冒国家风险”
(来自可调试的博客,关键词“Node.JS 未捕获异常”排名第 3)
"...由于 throw 在 JavaScript 中的工作原理,几乎没有任何方法可以安全地“从上次中断的地方继续”,而不会泄漏引用或创建其他类型的未定义的脆弱状态。响应抛出的错误最安全的方法是关闭进程。当然,在正常的 Web 服务器中,您可能打开了许多连接,并且突然关闭这些连接是不合理的,因为错误是由其他人触发。更好的方法是向触发错误的请求发送错误响应,同时让其他人在正常时间完成,并停止在该工作人员中侦听新请求”
Number4:集中处理错误,通过但不在中间件内
TL;DR: 错误处理逻辑,例如发给管理员的邮件和日志记录应该封装在一个专用且集中的对象中,所有端点(例如 Express 中间件、cron 作业、单元测试)出现错误时调用。
否则:不在一个地方处理错误会导致代码重复,并可能导致错误处理不当
代码示例 - 典型的错误流程
//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(function (result) {
res.status(200).json(result);
}).catch((error) => {
next(error)
});
}
catch (error) {
next(error);
}
//Error handling middleware, we delegate the handling to the centrzlied error handler
app.use(function (err, req, res, next) {
errorHandler.handleError(err).then((isOperationalError) => {
if (!isOperationalError)
next(err);
});
});
博客引述:“有时较低级别除了将错误传播给调用者外,无能为力”
(来自 Joyent 博客,关键词“Node.JS 错误处理”排名第一)
"...您最终可能会在堆栈的多个级别处理相同的错误。这种情况发生在较低级别无法执行任何有用的操作时,只能将错误传播给它们的调用者,然后将错误传播给它的调用者,依此类推. 通常,只有顶级调用者知道适当的响应是什么,无论是重试操作、向用户报告错误还是其他。但这并不意味着您应该尝试将所有错误报告给单个顶级回调,因为该回调本身无法知道错误发生在什么上下文中”
编号 5:使用 Swagger 记录 API 错误
TL;DR:让您的 API 调用者知道可能会返回哪些错误,以便他们可以周到地处理这些错误而不会崩溃。这通常使用 REST API 文档框架(如 Swagger)来完成
否则: API 客户端可能决定崩溃并重新启动,只是因为他收到了一个他无法理解的错误。注意:API 的调用者可能是您(在微服务环境中非常典型)
博客引用:“你必须告诉你的调用者会发生什么错误”
(来自 Joyent 博客,关键词“Node.JS logging”排名第一)
...我们已经讨论了如何处理错误,但是当您编写一个新函数时,如何将错误传递给调用您的函数的代码? …如果您不知道会发生什么错误或不知道它们的含义,那么您的程序不可能是正确的,除非是偶然的。所以如果你正在编写一个新函数,你必须告诉你的调用者会发生什么错误以及它们的含义
Number6:当陌生人来到镇上时优雅地关闭进程
TL;DR:当发生未知错误(开发人员错误,请参阅最佳实践 #3)时,应用程序的健康状况存在不确定性。一种常见的做法是建议使用 Forever 和 PM2 等“重启”工具小心地重启进程
否则:当捕获到不熟悉的异常时,某些对象可能处于故障状态(例如,全局使用的事件发射器,由于某些内部故障而不再触发事件)和所有未来请求可能会失败或行为异常
代码示例 - 判断是否崩溃
//deciding whether to crash when an uncaught exception arrives
//Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(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 = function (error) {
return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
}
this.isTrustedError = function(error)
{
return error.isOperational;
}
博客引述:“关于错误处理的三种思想流派”
(来自博客 jsrecipes)
...关于错误处理主要有以下三种思路: 1. 让应用程序崩溃并重新启动。 2.处理所有可能的错误,永不崩溃。 3。两者之间的平衡方法
Number7:使用成熟的记录器来增加错误的可见性
TL;DR: 一套成熟的日志工具,如 Winston、Bunyan 或 Log4J,将加快错误发现和理解。所以忘记console.log吧。
否则:浏览console.logs 或手动浏览凌乱的文本文件而不使用查询工具或体面的日志查看器可能会让您忙于工作直到很晚
代码示例 - 运行中的 Winston 记录器
//your centralized logger object
var logger = new winston.Logger({
level: 'info',
transports: [
new (winston.transports.Console)(),
new (winston.transports.File)({ filename: 'somefile.log' })
]
});
//custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });
博客引用:“让我们确定一些要求(对于记录器):”
(来自博客strongblog)
...让我们确定一些要求(对于记录器):
1. 为每个日志行添加时间戳。这个很容易解释——你应该能够知道每个日志条目是什么时候发生的。
2. 日志格式应该易于人类和机器消化。
3. 允许多个可配置的目标流。例如,您可能正在将跟踪日志写入一个文件,但当遇到错误时,写入同一个文件,然后写入错误文件并同时发送电子邮件......
编号 8:使用 APM 产品发现错误和停机时间
TL;DR:监控和性能产品(又名 APM)主动评估您的代码库或 API,以便它们可以自动神奇地突出显示您遗漏的错误、崩溃和慢速部分
否则:您可能会花费大量精力来衡量 API 性能和停机时间,可能您永远不会知道在现实世界场景中哪些是您最慢的代码部分以及这些部分如何影响用户体验
博客引用:“APM 产品细分”
(来自博客 Yoni Goldberg)
"...APM 产品由 3 个主要部分组成:1. 网站或 API 监控 - 通过 HTTP 请求持续监控正常运行时间和性能的外部服务。可以在几分钟内完成设置。以下是一些选定的竞争者: Pingdom、Uptime Robot 和 New Relic
2。代码检测—— 产品系列需要在应用程序中嵌入代理,以受益于慢代码检测、异常统计、性能监控等功能。以下是少数选定的竞争者:New Relic、App Dynamics
3。运营智能仪表板——这些产品线专注于通过指标和精选内容帮助运营团队轻松掌握应用程序性能。这通常涉及聚合多个信息源(应用程序日志、数据库日志、服务器日志等)和前期仪表板设计工作。以下是一些选定的竞争者:Datadog、Splunk"
以上为缩短版 - see here more best practices and examples