p-limit map 已捕获错误,为什么仍有 unhandled rejection
p-limit7.3.2遍历输入中途抛错时,已排mapper的拒绝可能无人观察。7.3.3补上处理;旧版可先收集有界输入,再提交任务。
limit.map() 外层已经被 try/catch 接住,Node 仍然因为另一个 Mapper failed 拒绝退出。若输入不是普通数组,而是一个读到中途会抛错的同步迭代器,需要检查“消费输入”和“等待任务”之间的空隙。
p-limit 7.3.2 的 map 先用 Array.from 遍历输入,同时提交 mapper;遍历结束后才把这批 Promise 交给 Promise.all。迭代器提前抛错时,后一步没有执行,已经提交的 mapper 仍可能运行并拒绝。外层捕获到 Iterator failed,不代表这些任务的拒绝也有人观察。
function* inputs() {
yield 'first';
throw new Error('Iterator failed');
}
如果第一个任务随后拒绝,便可能出现先捕获迭代错误、再发生未处理拒绝的两个结果。mapper 只返回失败、而输入正常结束的路径,不需要用这个机制解释。
采用包含回收处理的正式版本
正式 p-limit 7.3.3 已为遍历错误增加处理:记录已经生成的 Promise,在 Array.from 抛错时给它们登记拒绝处理,再重新抛出原来的迭代错误。它解决的是拒绝无人观察,不会把 mapper 变成成功,也不会自动取消已开始的工作。
按项目兼容要求升级,并核对锁文件与运行产物实际使用的 p-limit 副本。仅更新一个顶层依赖,不保证内部工具加载的另一份副本一起改变。
无法立即更新、且输入规模有明确上限时,可以先完整收集输入,成功以后再提交任务:
export async function mapCollected(limit, iterable, mapper) {
const items = Array.from(iterable);
return limit.map(items, mapper);
}
这里 limit 是已经创建好的 p-limit 实例。迭代阶段若失败,还没有因为本函数提交 mapper。代价是输入必须先占用内存,失去边遍历边排队的行为;它不适合无限迭代器或不可控的大数据源,也不能消除迭代器自身已经产生的外部副作用。
错误收集与取消仍是两件事
不要用全局 unhandledRejection 监听器把全部拒绝吞掉,也不要把 mapper 的 catch 写成返回 undefined,悄悄让失败任务混进成功结果。任务需要取消时,要沿原业务支持的 AbortSignal 或其他协议处理,并分别观察已开始和未开始的任务。
验收应覆盖输入在第一项之前抛错、提交一项后抛错、mapper 单独拒绝,以及全部成功。保留应用实际的未处理拒绝策略,确认迭代错误仍能交给调用方、已排任务的拒绝不再无人观察,正常结果顺序没有改变。
若 mapper 会写入数据库或提交远程任务,外层 map 失败不能证明这些工作没有发生。决定重试前先核对已完成项,不重放整批非幂等操作。


