1226 字
6 分鐘

Promise.allSettled 怎麼用?多個 API 部分失敗時保留成功結果

儀表板同時讀取帳號、通知與推薦內容時,如果通知 API 失敗就把整頁換成錯誤畫面,成功取得的帳號資料也會被藏掉。當這些資料可以各自顯示,Promise.allSettled() 能讓程式拿到每一項成功或失敗的結果,再替不同區塊決定 fallback。

資料彼此獨立、需要保留部分成功結果時用 allSettled;必須全數成功才能繼續時用 Promise.all。 allSettled 不會替請求設定 timeout、限制並行數或取消其他工作,這些仍要在任務本身設計。

結果依輸入順序排列,先替工作命名#

MDN 說明,每個結果都有 status:成功時是 fulfilled 並含 value,失敗時是 rejected 並含 reason。結果順序依輸入順序,不依完成速度。

只記住「第零項是帳號」很容易在新增 API 後拿錯資料。下面讓每個任務先有名稱,並在同一個索引上配回結果:

async function settleNamed(tasks) {
const names = tasks.map((task) => task.name);
if (new Set(names).size !== names.length) {
throw new Error('task names must be unique');
}
const results = await Promise.allSettled(
tasks.map((task) => Promise.resolve().then(() => task.run())),
);
return Object.fromEntries(results.map((result, index) => [
names[index], result,
]));
}

這是文章自己的 wrapper,並非新的內建 API。Promise.resolve().then(...) 讓 run() 在 Promise 鏈中執行,即使它同步 throw,也會成為該任務的 rejected 結果;若直接寫 tasks.map(task => task.run()),同步例外可能在進入 allSettled 前就中斷整個 map。

範例拒絕重複名稱,避免 Object.fromEntries 讓後面的結果蓋掉前面的結果。正式專案可再用 TypeScript 定義各任務的資料型別;先有明確對應,才知道畫面上的錯誤屬於哪個來源。

fetch 要自己判斷 HTTP 錯誤與逾時#

fetch 遇到 HTTP 404、500 不會自動 reject。若沒有檢查 response.ok,allSettled 可能把錯誤頁面當作成功資料。以下 helper 在每次呼叫時建立新的 timeout signal:

async function fetchJson(url, timeoutMs = 3_000) {
const response = await fetch(url, {
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
}
const sections = await settleNamed([
{ name: 'profile', run: () => fetchJson('/api/profile') },
{ name: 'notifications', run: () => fetchJson('/api/notifications') },
{ name: 'recommendations', run: () => fetchJson('/api/recommendations') },
]);
if (sections.profile.status === 'fulfilled') {
console.log('帳號資料', sections.profile.value);
} else {
console.error('帳號區塊載入失敗', sections.profile.reason);
}

三個 /api/ 路徑是產品整合示意,請換成實際 endpoint。UI 可讓通知區顯示重試按鈕、推薦區顯示預設內容,帳號區則依產品需求決定是否為必要資料。

必要與選用資料不一定要放在同一組等待:如果帳號決定權限,先確認帳號,再載入依賴該權限的內容;若希望各區塊一完成就呈現,可以在個別任務完成時更新 UI。allSettled 的聚合 Promise 會等全部結束,並不提供逐項串流結果。

timeout 也包含回應本文讀取。詳細的取消原因與 signal 生命週期可看 fetch timeout 的處理方式。重試時要建立新請求與新 signal,不能重用已中止的物件。

用可控制的任務驗證順序與部分失敗#

把 settleNamed 放入 settled-check.mjs,附上以下測試,再執行 node settled-check.mjs。本次維護流程已在 Node.js v24.13.0 執行:

import assert from 'node:assert/strict';
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const completed = [];
const data = await settleNamed([
{ name: 'slow', run: async () => {
await wait(20);
completed.push('slow');
return 42;
} },
{ name: 'broken', run: () => { throw new Error('unavailable'); } },
{ name: 'fast', run: async () => {
completed.push('fast');
return 7;
} },
]);
assert.deepEqual(completed, ['fast', 'slow']);
assert.deepEqual(Object.keys(data), ['slow', 'broken', 'fast']);
assert.equal(data.slow.value, 42);
assert.equal(data.broken.status, 'rejected');
assert.equal(data.broken.reason.message, 'unavailable');
assert.equal(data.fast.value, 7);
assert.deepEqual(await settleNamed([]), {});
await assert.rejects(settleNamed([
{ name: 'same', run: () => 1 },
{ name: 'same', run: () => 2 },
]));
console.log('allSettled checks passed');

另以本機 HTTP 伺服器確認了成功 JSON、HTTP 503 與慢回應 timeout 可以一起保留各自結果。這些驗證處理的是 JavaScript 與請求邊界,沒有測試儀表板畫面、登入狀態或跨來源請求。

allSettled 仍會卡住,也不會替你省請求#

只要有一項 Promise 永遠 pending,聚合就會一直等待。因此 timeout 要放在會停滯的任務上;只在外面用 Promise.race 停止等待,底層工作仍可能繼續執行。

另外,tasks.map 會把所有工作建立出來。十項和一千項都不會因為 allSettled 自動變成每次五項;大量請求要另用批次或 concurrency limiter。也不要用這個 wrapper 把必須依序執行的資料庫寫入、建立訂單和付款改成並行。

Promise.all 在第一項 reject 時會較早回報錯誤,但沒有取消其他 Promise。選擇兩者的核心是結果處理方式,不能拿它們作為資源取消或併發控制機制。

要上線前,至少讓每個失敗結果都有對應的 UI 或紀錄。若只是把 rejected 全部濾掉,沒有說明缺了哪些資料,使用者看到的「成功畫面」仍可能是不完整的。

參考資料:

MDN:Promise.allSettled()

MDN:Promise.all()

MDN:Using the Fetch API

MDN:AbortSignal.timeout()

Promise.allSettled 怎麼用?多個 API 部分失敗時保留成功結果
https://laplusda.com/posts/javascript-promise-allsettled-partial-failure/
作者
Zero
發佈於
2026-10-03
許可協議
CC BY-NC-SA 4.0