Promise.finally 清理流程:普通返回值会透传,清理失败却可能覆盖原错误

10-01 3阅读

请求结束后关闭加载提示,常常适合放进 Promise.finally,因为成功和失败都需要执行同一段收尾。但“无论结果如何都清理”并不等于“清理永远不会改变结果”。如果清理函数自己抛出异常,调用方最后看到的可能已经不是请求原本的错误。

理解这个差异,要区分原 Promise 和 finally 返回的新 Promise。原对象的状态不会被重写,后续调用方通常等待的是新对象。清理的返回方式决定新对象何时结束,以及继续传播原结果还是转向新的失败。

Promise.finally 清理流程:普通返回值会透传,清理失败却可能覆盖原错误

AI概念配图,非真实界面:以抽象物件说明本文主题,不代表运行结果。

用四组可运行断言观察结果

下面代码保存为 demo.mjs,用支持标准 Promise 的 Node.js 运行。它先检查同步清理的调用次数和实参,再用一个由测试手动释放的等待点控制异步清理。整个示例不访问网络,也不依赖固定毫秒数,因此不会因机器偶尔变慢而误判。

清理函数返回普通值时,这个值不会变成成功结果;原本拒绝的请求也不会因此恢复为成功。真正需要把异常转换为备用结果时,应在 catch 中表达恢复策略,避免把清理回调写成同时承担结果转换的隐蔽分支。

import assert from "node:assert/strict";

let calls = 0;
const value = await Promise.resolve(7).finally((...args) => {
  calls += 1;
  assert.equal(args.length, 0);
  return 999;
});
assert.equal(value, 7);
assert.equal(calls, 1);
const original = new Error("request failed");
await assert.rejects(
  Promise.reject(original).finally(() => "ignored"),
  (error) => error === original
);

let release;
let entered;
const gate = new Promise((resolve) => { release = resolve; });
const started = new Promise((resolve) => { entered = resolve; });
const events = [];
const pending = Promise.resolve(42).finally(async () => {
  events.push("cleanup start");
  entered();
  await gate;
  events.push("cleanup end");
  return "ignored too";
}).then((result) => {
  events.push("result " + result);
  return result;
});
await started;
assert.deepEqual(events, ["cleanup start"]);
release();
assert.equal(await pending, 42);
assert.deepEqual(events, ["cleanup start", "cleanup end", "result 42"]);

const cleanupError = new Error("cleanup failed");
await assert.rejects(
  Promise.resolve(7).finally(() => { throw cleanupError; }),
  (error) => error === cleanupError
);
await assert.rejects(
  Promise.reject(original).finally(() => Promise.reject(cleanupError)),
  (error) => error === cleanupError
);
console.log(events.join(" -> "));
console.log("pass-through, async wait and cleanup-failure checks passed");

等待清理完成,也意味着承担它的失败

异步部分先记录进入清理,再等待 gate;释放之后才记录清理结束,最后下游拿到原来的数值。断言检查整条顺序,说明调用方若等待 finally 返回的新对象,清理工作就是这条链的一部分,而不是在旁边随意启动的后台任务。

如果清理函数启动异步操作却忘记返回对应 Promise,外层无法知道它还没完成。使用 async 回调并在内部 await,或直接返回清理 Promise,才能把完成时机接起来。仅仅在回调里调用一个异步函数,不会自动让外层等待它。

最后两组分别验证同步抛错与返回拒绝对象:前者把原成功变为清理失败,后者让原失败被清理失败取代。因此“finally 一定保留原结果”是不完整的说法。它只有在清理正常完成时才按这里演示的方式传播原来的值或原因。

为清理失败选择明确的业务政策

关闭页面动画与释放关键资源的重要程度不同。非关键的收尾失败,可以在清理内部捕获并记录后继续传播原结果;关键清理失败则可能必须让整个任务失败。选择应来自业务约定,而不是为了让控制台安静就统一吞掉所有异常。

如果原任务和关键清理都失败,而排查需要两份原因,可以在更明确的控制流程中分别保存并组合错误。不要只依赖 finally 后面的一个 catch 猜测前面发生了什么,因为被覆盖的原错误不会自动作为新错误的原因附带回来。

还要注意链的位置:在 catch 恢复之前和之后接 finally,清理观察到的流程阶段不同。本文回调不接收成功值或错误原因,只负责收尾;需要根据结果采取不同业务动作时,应使用相应的成功或失败分支,并保持最终链被返回或等待。

参考资料

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