809 字
4 分鐘

Promise finally 蓋掉原本錯誤?保留工作與清理失敗的原因

本文目錄

writeFile().finally(closeFile) 裡,寫入和關閉都失敗時,呼叫端會收到哪個錯誤?**.finally() 回傳的新 Promise 會以清理失敗的原因拒絕。**原本的工作 Promise 仍保有自己的拒絕原因,但只捕捉整條鏈時,容易漏掉它。

MDN與 ECMAScript 演算法說明:finally callback 成功完成時,原結果往後傳;callback 拋錯或回傳 rejected Promise,才會用新的原因拒絕。這裡用工作與清理各一個函式,明確選擇雙重失敗的處理政策。

先決定雙重失敗時要給呼叫端什麼#

工作清理本文 wrapper 的結果
成功成功原工作值
失敗成功原工作錯誤
成功失敗清理錯誤
失敗失敗AggregateError,依序放工作與清理錯誤

這適合清理必須完成、而呼叫端能查看多個原因的工作。如果清理只是 best effort,則可在清理內捕捉並記錄錯誤,讓原工作結果保持不變;記錄管道本身也須有失敗政策。既有呼叫端若只按單一錯誤類型分支,採用 aggregate 前要一起調整。

下面的 withCleanup() 接收工作函式,因此同步拋錯和非同步拒絕都會進入同一條流程。用獨立布林值記錄工作失敗,才能正確處理 Promise.reject(undefined)。

一份可直接跑的錯誤保留範例#

存成 cleanup.mjs,執行 node cleanup.mjs。範例使用假工作與假清理,沒有操作檔案或連線:

import assert from 'node:assert/strict';
async function withCleanup(work, cleanup) {
let value;
let workFailed = false;
let workError;
try {
value = await work();
} catch (error) {
workFailed = true;
workError = error;
}
try {
await cleanup();
} catch (cleanupError) {
if (workFailed) {
throw new AggregateError([workError, cleanupError], '工作與清理都失敗');
}
throw cleanupError;
}
if (workFailed) throw workError;
return value;
}
const writeError = new Error('write failed');
const closeError = new Error('close failed');
const ok = () => 42;
const failWrite = () => Promise.reject(writeError);
const failClose = () => Promise.reject(closeError);
await assert.rejects(failWrite().finally(failClose), e => e === closeError);
assert.equal(await Promise.resolve(42).finally(() => 99), 42);
assert.equal(await withCleanup(ok, () => 99), 42);
await assert.rejects(withCleanup(failWrite, () => {}), e => e === writeError);
await assert.rejects(withCleanup(ok, failClose), e => e === closeError);
await assert.rejects(withCleanup(failWrite, failClose), e => {
assert.ok(e instanceof AggregateError);
assert.deepEqual(e.errors, [writeError, closeError]);
return true;
});
await assert.rejects(withCleanup(() => Promise.reject(undefined), failClose), e => {
assert.deepEqual(e.errors, [undefined, closeError]);
return true;
});
await assert.rejects(withCleanup(() => { throw writeError; }, () => {}),
e => e === writeError);
console.log('finally 與清理政策檢查通過');

第一個斷言重現原錯誤被清理錯誤取代;第二個確認成功 callback 的普通回傳值會被忽略。後面覆蓋 wrapper 的四種組合、空拒絕原因與同步拋錯。wrapper 會等待 cleanup,若它永遠不 settle,整個呼叫也會一直等待;真實資源另需自己的逾時或取消設計。

清理開始的時間也要獨立確認#

這個 wrapper 適用單一工作已結束後的清理。若 work() 裡的 Promise.all() 提早拒絕,其他工作可能仍在使用同一個資源;先把所有持有者收斂完成,再關閉資源。這是 Promise.all 失敗後等待清理處理的另一個問題。

2026-11-11 的本機驗證在 macOS、Node.js 24.13.0 執行以上完整程式通過。它驗證的是 Promise 與錯誤傳遞;接到資料庫、檔案或瀏覽器元件後,仍需補上該資源的完成與清理條件。

參考資料:

MDN:Promise.prototype.finally()

ECMAScript:Promise.prototype.finally

MDN:AggregateError

Promise finally 蓋掉原本錯誤?保留工作與清理失敗的原因
https://laplusda.com/posts/javascript-promise-finally-preserve-errors/
作者
Zero
發佈於
2026-11-11
許可協議
CC BY-NC-SA 4.0