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()