Node.js 异常断言:async 函数写了 throw,为什么 assert.throws 仍说没有异常

前天 3阅读

给一个async函数写测试,函数第一句就throw,assert.throws却报告缺少预期异常。关键不在错误出现得够不够早,而在调用接口:async函数把失败表达为返回Promise的拒绝,同步异常断言不会替你等待这个Promise。

完整程序已在Node.js v24.19.0执行,保存为demo.mjs并运行node demo.mjs。模块使用顶层await和内置断言,不访问文件或网络。每个故意产生的失败都被相应断言接住,实验没有留下未处理的拒绝。

Node.js 异常断言:async 函数写了 throw,为什么 assert.throws 仍说没有异常

AI模型生成概念图:直接抛出的信号和经过Promise通道的拒绝进入不同捕获网,时钟只比喻等待边界,不表示固定延迟;不是测试工具截图。

完整实验程序

failSync与failAsync抛出相同名称、相同消息的错误,只在声明上相差async。这样测试差异可以归到完成协议,而不是混入不同异常类型或额外定时器。

完整可运行程序

import assert from 'node:assert/strict';

function failSync() {
  throw new RangeError('outside range');
}
async function failAsync() {
  throw new RangeError('outside range');
}

assert.throws(failSync, { name: 'RangeError', message: 'outside range' });
console.log('synchronous throws:', 'matched');

const pending = failAsync();
const rejectionCheck = assert.rejects(pending,
  { name: 'RangeError', message: 'outside range' });
assert.throws(
  () => assert.throws(() => pending, RangeError),
  { code: 'ERR_ASSERTION' }
);
console.log('throws on Promise:', 'missing synchronous exception');
await rejectionCheck;
console.log('rejects on Promise:', 'matched');

await assert.rejects(failAsync,
  { name: 'RangeError', message: 'outside range' });
console.log('rejects on async function:', 'matched');

const original = new Error('synchronous setup failure');
let validatorCalled = false;
const check = assert.rejects(
  () => { throw original; },
  () => { validatorCalled = true; return true; }
);
await assert.rejects(check, error => error === original);
assert.equal(validatorCalled, false);
console.log('sync callback passed to rejects:', 'validatorCalled=' + validatorCalled);

本次实际输出

synchronous throws: matched
throws on Promise: missing synchronous exception
rejects on Promise: matched
rejects on async function: matched
sync callback passed to rejects: validatorCalled=false

断言要匹配函数的失败通道

第一行synchronous throws匹配成功,说明普通函数把RangeError直接抛回调用栈。随后pending保存async函数返回的Promise,assert.throws的内层回调只是把这个Promise返回,因此内层断言产生ERR_ASSERTION。

外层assert.throws捕获的是这次断言失败,验证确实没有同步异常穿出来;它不是在证明RangeError已经被同步捕获。输出missing synchronous exception是程序为这个已验证结果打印的固定说明,避免依赖完整报错堆栈的版本格式。

先接住拒绝,再观察错误用法

rejectionCheck在测试错误用法之前就通过assert.rejects连接到pending。随后await rejectionCheck等待对RangeError名称与消息的验证完成,第三行才打印matched。这个安排让教学反例不会留下无人处理的Promise拒绝。

第四行直接把failAsync函数交给assert.rejects,也能匹配。该入口会调用函数并等待它返回的Promise,所以通常更便于把一次异步操作完整放进测试。无论传Promise还是传函数,都要等待assert.rejects自己返回的Promise,才能把断言结果纳入测试结束条件。

普通函数同步抛错,不等于返回拒绝

最后一个回调不是async函数,它在返回Promise之前直接抛出original。Node文档规定这时assert.rejects把该错误作为自身返回Promise的拒绝原因,并跳过提供的错误匹配器。validatorCalled=false验证了匹配器根本没有执行。

外层await assert.rejects(check, ...)专门验证这条检查链仍以original对象拒绝,所以脚本最终正常结束。若只把同步函数交给assert.rejects并等待,它不会因为错误类型看似正确就自动通过原来的匹配规则。同步失败应使用assert.throws;混合接口则要先明确自己的失败协议。

成功打印与测试完成不是同一时刻

异步断言若缺少await或没有把Promise交回测试框架,后面的日志可能先打印,断言失败随后才出现。程序里每条matched都位于对应检查完成之后,这让输出可以作为清楚的执行证据。

这里验证了错误名称、消息以及最后一例的对象身份,没有只检查“发生过任意错误”。实际测试还应确认触发的是想测试的分支,而不是准备数据时先出现另一个异常。不同失败阶段给出不同样本,能减少测试碰巧通过的情况。

断言工具不会自动取消尚未结束的工作、关闭文件或恢复业务数据。本例只有立即完成的小函数,不涉及这些资源。把错误传播测试与必要的收尾检查分别写清,才能避免看到一个拒绝就误认为整项任务已经结束。

参考资料

官方资料核验日期:2026年10月2日。上述输出来自本文完整程序的本地执行,断言全部通过,退出状态为零。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。