ESLint prefer-promise-reject-errors 规则深度解析:强制用 Error 对象作为 Promise 拒绝原因 ESLint prefer-promise-reject-errors 规则深度解析强制用 Error 对象作为 Promise 拒绝原因【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint本篇技术指南围绕 ESLint 内置规则prefer-promise-reject-errors展开详细讲解该规则的设计动机、allowEmptyReject配置项、完整的不正确/正确代码示例并结合仓库源码剖析其 AST 检测原理与静态分析的固有局限。读完本文你将掌握如何在实际项目中配置并使用该规则理解它如何与no-throw-literal规则协同以及何时应当关闭它。规则背景为什么要用 Error 对象拒绝 Promise在 JavaScript 中Promise 的reject()函数可以接收任意值作为拒绝原因rejection reason。然而工程实践中有一条公认的良好实践对于用户自定义错误只向reject()传递内置Error对象的实例。这条规则的核心依据在于Error对象的一个重要特性它会自动记录构造时的堆栈追踪信息stack trace开发者可以借此定位错误究竟源自何处。相反如果 Promise 被一个非Error值如字符串、数字拒绝堆栈信息会丢失排查问题时将难以判断拒绝发生的具体位置——控制台只会显示Unhandled Promise Rejection: something bad happened这样近乎无用的信息。该规则在仓库中的元数据定义于 lib/rules/prefer-promise-reject-errors.js属于suggestion建议型规则默认不启用recommended: false不可自动修复fixable: null。其官方文档位于 docs/src/rules/prefer-promise-reject-errors.md。规则目标该规则旨在确保 Promise 只以Error对象作为拒绝原因。规则的核心消息为Expected the Promise rejection reason to be an Error.对应源码中的rejectAnError消息 IDmessages: { rejectAnError: Expected the Promise rejection reason to be an Error., },它同时覆盖两条检测路径Promise.reject(value)静态方法调用new Promise((resolve, reject) { reject(value) })构造器中调用第二个参数reject的情形。配置选项allowEmptyReject该规则接受一个可选的对象参数仅包含一个布尔选项选项类型默认值说明allowEmptyRejectbooleanfalse允许无参调用Promise.reject()/reject()在 lib/rules/prefer-promise-reject-errors.js 的defaultOptions中该选项的默认值被定义为{ allowEmptyReject: false }且 schema 规定该对象不允许出现其他额外属性schema: [ { type: object, properties: { allowEmptyReject: { type: boolean }, }, additionalProperties: false, }, ],补充说明无参的Promise.reject()实际会将undefined作为拒绝原因这也是为什么默认情况下Promise.reject()会被报告——它等价于拒绝了一个非Error值。不正确的代码示例以下代码在默认配置error级别下均会被报告/*eslint prefer-promise-reject-errors: error*/ Promise.reject(something bad happened); Promise.reject(5); Promise.reject(); new Promise(function(resolve, reject) { reject(something bad happened); }); new Promise(function(resolve, reject) { reject(); });正确的代码示例以下代码符合规则要求不会被报告/*eslint prefer-promise-reject-errors: error*/ Promise.reject(new Error(something bad happened)); Promise.reject(new TypeError(something bad happened)); new Promise(function(resolve, reject) { reject(new Error(something bad happened)); }); const foo getUnknownValue(); Promise.reject(foo);注意最后一个例子foo来自未知来源函数调用返回值静态分析无法确定它是否一定是非Error值因此规则选择不报告详见下文已知限制一节。开启 allowEmptyReject 后的正确代码示例/*eslint prefer-promise-reject-errors: [error, {allowEmptyReject: true}]*/ Promise.reject(); new Promise(function(resolve, reject) { reject(); });源码级原理规则是如何检测的深入阅读 lib/rules/prefer-promise-reject-errors.js可以看到该规则的实现由三个核心部分组成。1.checkRejectCall检查 reject 调用的实参这是规则的判定核心lib/rules/prefer-promise-reject-errors.js#L62-L81function checkRejectCall(callExpression) { if (!callExpression.arguments.length allowEmptyReject) { return; } const rejectionReason callExpression.arguments[0]; if ( !callExpression.arguments.length || !astUtils.couldBeError(rejectionReason) || (rejectionReason.type Identifier rejectionReason.name undefined sourceCode.isGlobalReference(rejectionReason)) ) { context.report({ node: callExpression, messageId: rejectAnError, }); } }报告条件有三种调用无参数且未开启allowEmptyReject第一个参数不可能是一个Error对象couldBeError返回false第一个参数是全局的undefined标识符这里用isGlobalReference排除了被局部遮蔽的undefined因为局部遮蔽的undefined理论上可能是一个Error对象。2.isPromiseRejectCall识别Promise.reject()调用lib/rules/prefer-promise-reject-errors.js#L88-L99function isPromiseRejectCall(node) { return ( astUtils.isSpecificMemberAccess( node.callee, Promise, reject, ) sourceCode.isGlobalReference( astUtils.skipChainExpression(node.callee).object, ) ); }这里有两层判断第一调用必须是Promise.reject这样的成员访问形式同时兼容可选链如Promise?.reject(5)测试中将其作为违规用例第二Promise必须是全局引用。也就是说如果代码中声明了局部变量遮蔽了全局Promise规则不会误报。这一点在测试文件 tests/lib/rules/prefer-promise-reject-errors.js 中有大量覆盖例如// 以下均为 valid不报告 /* global Promise:off */ Promise.reject(x), let Promise; Promise.reject(x);, function f(Promise) { return Promise.reject(x); }, { class Promise { static reject(x) { return x; } } Promise.reject(x); },3.NewExpression:exit监听器追踪构造器中的 reject规则通过监听NewExpression:exit来识别new Promise((resolve, reject) ...)lib/rules/prefer-promise-reject-errors.js#L118-L161。之所以选择:exit而非进入阶段是为了确保表达式内的节点已挂载parent属性便于后续沿引用关系回溯。其处理流程是确认callee是全局引用的Promise标识符且第一个参数是函数、参数个数大于 1、第二个参数是普通标识符因此解构形式的第二参数如function(resolve, {apply})不会被误判通过sourceCode.getDeclaredVariables找到与第二个参数同名的变量过滤出读取该变量的引用ref.isRead()过滤出作为函数调用 callee的引用即reject(...)形式对每个这样的调用执行checkRejectCall。值得注意的边界处理当new Promise构造器的第二个参数名与第一个相同如function(reject, reject)或与arguments同名时测试中均作为违规用例覆盖。从源码注释可以看到作者已论证重复参数名场景下不会出现变量被覆盖前就被读取的误判因为带重复参数的函数不允许在参数列表中使用解构或默认值。核心辅助函数couldBeError什么可能是 ErrorcheckRejectCall依赖的astUtils.couldBeError定义于 lib/rules/utils/ast-utils.js#L2529-L2594它采用宁可放过、不可误报的宽松策略对以下节点类型直接返回true即有可能是 Error 对象Identifier、CallExpression、NewExpression、MemberExpressionTaggedTemplateExpression、YieldExpression、AwaitExpression、ChainExpression对于复合表达式则递归判断赋值表达式和只看右侧||和??左右任一成立即可其余数学赋值运算符算术/位运算结果只能是原始值或抛异常故返回false序列表达式只关心最后一个表达式逻辑表达式只看右侧短路语义下左侧假值不可能是 Error||左右任一成立即可条件表达式consequent与alternate任一成立即可。正因如此Promise.reject(foo new Error())会被报告数学赋值结果不可能是 Error而Promise.reject(foo new Error())则合法——这些都被测试用例逐一验证。已知限制静态分析的边界由于静态分析的固有局限该规则无法保证你只会用Error对象拒绝 Promise。规则只会在能确定拒绝原因显然不是Error时报告当某个原因是否为Error存在不确定性时规则选择不报告。例如以下代码会通过该规则的检查但实际运行时拒绝原因并不是Errorconst err error; Promise.reject(err); // err 被推断为可能是 ErrorIdentifier不报告 function foo(bar) { return bar; } Promise.reject(foo(error)); // CallExpression 被视为可能不报告这一局限与no-throw-literal规则完全一致——后者同样基于couldBeError判断只禁止抛出不可能是Error的表达式字符串、数字、null、undefined等字面量。与 no-throw-literal 的分工为避免规则间冲突prefer-promise-reject-errors不会报告 async 函数中throw非 Error 值的情况——尽管这最终会导致 Promise 拒绝async 函数的throw会被转化为 rejected promise。这类场景应交给no-throw-literal规则处理async function foo() { throw something bad happened; // no-throw-literal 会报告prefer-promise-reject-errors 不处理 }两个规则在文档 front-matter 中被标记为related_rules关联关系见 docs/src/rules/prefer-promise-reject-errors.md 头部并都引用 Bluebird 的警告说明作为设计参考。实际配置建议在扁平配置flat config中启用该规则可以像下面这样在eslint.config.js中配置export default [ { rules: { prefer-promise-reject-errors: [error, { allowEmptyReject: false }], }, }, ];针对不同团队的取舍建议默认推荐保持allowEmptyReject: false强制所有拒绝都携带明确的Error对象最大化调试信息宽松场景如果代码中大量使用拒绝即代表流程终止的模式如取消操作可开启allowEmptyReject: true允许无参拒绝自定义拒绝值如果团队约定使用自定义的非Error值如业务错误码对象作为拒绝原因则应关闭此规则。When Not To Use It何时不使用如果你正在使用自定义的非Error值作为 Promise 拒绝原因可以直接关闭该规则。典型场景包括团队内部约定用普通对象{ code, message }表示业务错误、依赖第三方库返回特殊拒绝值、或者拒绝原因仅用于流程控制而不需要堆栈信息等。结语prefer-promise-reject-errors是 ESLint 内置的 suggestion 型规则它用一个简单的约定只用Error对象拒绝 Promise换取可追踪的堆栈信息显著降低异步错误排查成本。理解其基于couldBeError的可能即放过判定策略有助于准确把握它的报告边界而结合no-throw-literal一起使用则能覆盖同步与异步两条错误传播路径构建更完整的异常质量防线。延伸阅读仓库内规则实现源码lib/rules/prefer-promise-reject-errors.js规则单元测试tests/lib/rules/prefer-promise-reject-errors.js核心判定工具函数couldBeErrorlib/rules/utils/ast-utils.js关联规则no-throw-literal文档docs/src/rules/no-throw-literal.md全部内置规则索引lib/rules/index.js【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考