三つのAPIをまとめて読み込み、一つが失敗したのでエラー画面にした。ところがサーバーログを見ると、残りの処理はまだ動いている。もう一度読み込むと、前の処理と次の処理が重なってしまう。
Promise.allの失敗は、すでに始まった仕事を全部取り消す合図ではありません。集めていた結果を、全件成功として受け取れなくなったという通知です。この違いを知らないと、画面の失敗と裏側の中止を混同してしまいます。
三つ目の処理を手動で完了させる実験で、allの失敗後にも処理が続くことを確認します。ゴールは、全件が必要なのか、部分結果を使うのか、処理そのものを中止したいのかを分けて、集約方法を選べることです。
Promiseとasync/awaitを使える方が対象です。執筆時はWindows・Node.js v24.19.0で実行し、その後Linux・Node.js v24.19.0(2026年11月5日)でも同じ出力を再確認しました。外部APIは使わず、時間差やネットワークの偶然に依存しない形で観察します。
先に決めるのは「一件失敗した画面をどうするか」
たとえば、請求額を確定するために必須のデータが三つあるなら、一つ欠けた合計を完成値として見せられません。一方、独立した三つのダッシュボードなら、失敗したパネルだけ再読み込みにして、成功した二つを使いたいかもしれません。
| 必要な結果 | 検討する方法 | 別途決めること |
|---|---|---|
| 全件が成功した値のまとまり | Promise.all |
一件失敗した後、残りをどう扱うか |
| 成功・失敗を含む各件の一覧 | Promise.allSettled |
失敗した項目の表示と再実行 |
| 開始済みの処理そのものを止めたい | 各処理に対応するキャンセル | 中止要求が何を止め、何を止めないか |
allとallSettledは、結果をどう受け取るかの違いです。キャンセル機能への切り替えではありません。
Aは成功、Bは失敗、Cはまだ終わっていない状態を作る
空の練習用フォルダへ次をaggregate.mjsとして保存し、node aggregate.mjsを実行します。追加パッケージは不要です。
import assert from 'node:assert/strict';
let finishSlow;
let slowFinished = false;
const slow = new Promise(resolve => { finishSlow = resolve; })
.then(value => { slowFinished = true; return value; });
const tasks = [Promise.resolve('A'), Promise.reject(new Error('B failed')), slow];
const all = Promise.all(tasks);
const settled = Promise.allSettled(tasks);
await assert.rejects(all, /B failed/);
assert.equal(slowFinished,false);
console.log('all rejected; C is still unfinished');
finishSlow('C');
const results=await settled;
assert.equal(slowFinished,true);
assert.deepEqual(results.map(x=>x.status),['fulfilled','rejected','fulfilled']);
assert.equal(results[0].value,'A'); assert.equal(results[2].value,'C');
console.log('allSettled:',results.map(x=>x.status).join(', '));
console.log('values:',results[0].value,results[1].reason.message,results[2].value);
console.log('PASS: remaining work completed after aggregate rejection');
コードのfinishSlowは、Cを完了させるために外へ取り出したresolve関数です。本番向けのAPI設計例ではなく、「まだ終わらせていない処理がある」と確実に分かるようにする実験装置です。
まずAは成功、Bは失敗し、Cだけを未完了にします。Promise.allの失敗を確認した時点では、slowFinishedはfalseです。その後にfinishSlow('C')を呼ぶと、Cは完了し、allSettledの結果へ入ります。
実際の出力です。
$ node aggregate.mjs
all rejected; C is still unfinished
allSettled: fulfilled, rejected, fulfilled
values: A B failed C
PASS: remaining work completed after aggregate rejection
最初の行でallは失敗しています。それでも最後には、Cの結果まで得られています。失敗した集約が復活したのではなく、個々の処理が別々に進み、その完了をallSettledでも待っていたということです。
結果配列は、速く終わった順ではなく入力順に読む
allSettledの各要素にはstatusがあります。fulfilledならvalue、rejectedならreasonを読みます。この例では、A・B・Cの入力順で、成功・失敗・成功が返ります。
実アプリなら、各リクエストに対応するIDや表示欄を同じ順序で保持しておくと、どの失敗をどこへ表示するかが決められます。失敗した要素にまでvalueがあるつもりで扱わず、まずstatusで枝を分けます。
この実験でallとallSettledに渡しているのは、同じPromise配列です。結果を二通りで観察しているだけで、同じAPIを二回呼んでいるわけではありません。一方、API呼び出し関数をそれぞれの配列生成で二回実行すれば、仕事そのものが二回始まります。Promiseを渡すことと、仕事を始める関数を呼ぶことを区別してください。
allSettledへ変えただけでは、待ち続ける問題は解決しない
allSettledは全件が成功か失敗で落ち着くまで待ちます。そのため、永遠に完了しないものが一つあると、結果一覧も返りません。今回ならfinishSlow('C')を呼ばなければ、Cの完了を観察できません。
実アプリで長く待つことが問題なら、各処理がいつ打ち切られるべきかを別に決めます。必要に応じてタイムアウトやキャンセルを設計しますが、allSettledへ置き換えただけでそれらが追加されるわけではありません。
なお、allの失敗を誰も受け取らないままにすると、Node.js v24では未処理のrejectionとしてプロセスが終了します(このコードでawait assert.rejects(all, ...)の行を消して試すと確認できます)。「失敗しても後で見ればよい」ではなく、集約したPromiseは必ずawaitやcatchで受け取ります。
また、allのcatchで直ちに全件を再実行すると、旧Cが動いている間に新しいCを始める可能性があります。データを読むだけの処理と、決済や登録など副作用を持つ処理では、再実行の判断が異なります。
想定と違う結果になったら、集約と個別処理を分けて追う
| 症状 | 先に確かめること |
|---|---|
| allの失敗後にもサーバーの処理が続く | 集約の失敗と個別の中止を混同していないか。中止処理を実装したか |
| allSettledでいつまでも結果が返らない | 未完了のPromiseがどれか。各処理の完了・失敗の観測があるか |
| 結果が違うパネルへ表示される | 入力順と表示先IDの対応。完了順と誤認していないか |
| リクエストが二重に送られる | Promiseの再利用ではなく、生成関数を再度呼んでいないか |
| 失敗を全部無視して画面が完成扱いになる | 部分成功を許す項目と、必須項目を分けているか |
本記事で実行したのはPromiseの集約です。fetchのAbortSignalや、キャンセル後にサーバー側処理がどこまで止まるかは検証していません。ブラウザ側で待つのをやめたことを、サーバーの処理が取り消された証拠にはできません。
自分の画面へ戻ったら、まず各データを「全部必要」「部分的に使える」に分けてください。その後で、中止と再実行を別の項目として決めます。結果の集め方と仕事の寿命を分けると、一件失敗した後の動きまで説明できる設計になります。
参考資料(一次情報)
- ECMAScript Promise.all
- MDN Promise.all(「返されたPromiseのrejectは残りの処理を取り消さない」旨の記述あり)
- MDN Promise.allSettled
このテーマを自分でも試して学びたい方へ。プログラミング学習サービスのCodeNestでは、AIに質問しながらコードを書いて学べます(運営:Hatch IT)。
