この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。JavaScriptのPromise.allとPromise.allSettledの公式仕様を基に、架空の3処理を比較する教材を用意しました。検証ステータス:📘 公式仕様を参照・教材の実ブラウザ動作は未確認
3つの処理を同時に始めたとき、1つだけ失敗したら、成功した2つの結果まで捨てる必要があるのでしょうか。答えは、何を「成功」とみなすかによります。全件成功して初めて意味がある処理ならPromise.all、個々の成功と失敗を一覧に残したい処理ならPromise.allSettledが向いています。両者の違いは「どちらが速いか」よりも、失敗をどの単位で扱うかです。
まず安全に動かす
今回の題材は、架空の注文画面が「在庫確認」「価格取得」「配送確認」を並行して行う状況です。実際の注文、決済、ネットワークアクセスは行いません。JavaScriptのタイマーで成功・失敗を模擬するだけなので、架空の結果を繰り返し観察できます。
対象はNode.js 18以降です。標準機能だけを使い、追加パッケージやAPIキーは不要です。Daily-Code-Samplesの完成版とREADMEを開き、app.cjsとtest.cjsを同じフォルダーへ保存します。端末でnode app.cjsを実行すると、2番目の処理だけが失敗する設定で始まります。続いてnode app.cjs allを実行して、返る情報を比較します。最後にnode app.cjs settled successとすると、失敗する条件だけを変えられます。
この実験では出力の順序にも注目してください。タイマーの終了は価格取得、配送確認、在庫確認の順ですが、allSettledの結果配列は開始時に指定した入力順になります。終了が早かったものから並ぶと考えると、画面の表示先やCSVの列を取り違えます。結果のtask番号は、実行完了順ではなく元の配列の位置を表しています。
観察する結果
最初のallSettled実行では、1番目がfulfilled、2番目がrejected、3番目がfulfilledになります。成功した在庫確認と配送確認の値は残り、失敗した価格取得には理由が残ります。反対にall実行では、価格取得が失敗した時点で全体のPromiseが拒否され、catch側に価格取得:FAILEDが渡されます。成功した2件の値を同じ結果配列から取り出すことはできません。
ここで大事なのは、Promise.allが「ほかの処理を停止した」という意味ではないことです。allは結果をまとめるPromiseの状態を拒否にするだけで、すでに開始したタイマーや通信を自動的に中断しません。実際のHTTP要求を止めたいなら、対応するAPIでAbortControllerなどを別に設計する必要があります。単にallをallSettledへ置き換えても、キャンセルやタイムアウトが実装されるわけではありません。
どこで分岐するのか
次の図は、3件のうち1件が拒否されたときに、呼び出し側へ渡る情報の違いだけを示しています。左側の処理は同じでも、結果を集める方法によって後続の判断が変わります。
flowchart TD
A["3件の非同期処理を開始"] --> B{"集約方法"}
B -->|Promise.all| C["1件の拒否で集約結果はreject"]
B -->|Promise.allSettled| D["全件のsettledを待つ"]
C --> E["catchで全体エラーを扱う"]
D --> F["各件のfulfilled/rejectedを判定"]
図のall側は全体の戻り値についての説明です。個々の処理がそこで強制終了する図ではありません。allSettled側も、拒否が起きないことを保証するものではなく、拒否を結果オブジェクトとして受け取れるという違いです。非同期処理の「実行」と、その結果の「集約」を分けて考えると混乱しにくくなります。
コードの中心を読む
const results = await Promise.allSettled(tasks);
const summary = results.map((item, index) => ({
task: index + 1,
status: item.status,
detail: item.status === "fulfilled"
? item.value
: item.reason.message
}));
tasksは3つのPromiseを並べた配列です。awaitは、すべてが成功するまで待つのではなく、すべてが成功または失敗のどちらかに確定するまで待ちます。mapでは各結果を元の順序で取り出し、statusで値の取り出し先を切り替えています。成功した場合はvalue、拒否された場合はreasonです。失敗した結果に無条件でvalueを読みに行くと、必要なエラー情報を失います。
なおreasonが必ずErrorオブジェクトとは限りません。Promiseは文字列や数値でもrejectできるため、一般的なコードでreason.messageを無条件に読むのは危険です。今回の教材では自分でnew Error(...)を渡すと決めているので成立します。外部ライブラリや他人のコードから返るPromiseを扱うなら、instanceof Errorなどで型を確認して表示を整える必要があります。
1条件だけ変えて比較する
今度はnode app.cjs settled successを実行してください。変更するのは2番目の処理が失敗するかどうかだけです。すべてfulfilledになれば、先ほどのrejectedはallSettled自体の不具合ではなく、入力した失敗条件の結果だったと確認できます。node app.cjs all successなら、allでも3つの値を配列として受け取れます。つまり、正常系では両者とも結果を扱えますが、失敗系の契約が違います。
ここで「全部の結果を見たいから、常にallSettledが正解」とはなりません。注文の確定処理では価格が取得できないのに在庫と配送だけで決済を進めるのは危険です。一方、複数の独立したサーバーの稼働確認なら、1台の失敗を理由にほかの正常結果まで隠す必要はありません。求められる業務判断が違うため、適切な集約方法も変わります。
失敗したときの切り分け
何も表示されないときは、まずNode.jsのバージョンとファイル名、実行場所を確認します。Cannot find moduleなら保存場所やファイル名の問題が考えられます。Promise.allSettled is not a functionなら古いJavaScript実行環境を疑います。標準APIの対応状況を公式資料で確認し、環境を更新するか代替設計を検討してください。コードを直す前に、どの層で失敗しているかを分けることが重要です。
結果が想定と違う場合は、allという第1引数とsuccessという第2引数を混同していないか確認します。教材は第1引数で集約方式を、第2引数で失敗の有無を切り替えます。node app.cjs successだけでは第2引数を変更したことになりません。こうした小さな引数の取り違えは、非同期処理の仕様を誤解したように見える原因になります。
実業務では、拒否理由をそのまま画面やログへ出さないことも必要です。エラーに認証情報、内部URL、顧客データが含まれる場合があるからです。今回のdetailは架空の固定文字列だけですが、運用コードでは公開向けメッセージと内部診断情報を分け、秘密をマスクしてください。さらにタイムアウト、同時実行数の制限、再試行時の重複実行防止を設計しないと、結果を集められても安定した処理にはなりません。
業務での使い分け
独立した3つの監視対象から状態を集めるダッシュボードなら、allSettledで成功と失敗をそれぞれ表示すると、部分障害を隠さずに済みます。1件だけ失敗したときは、失敗対象の再確認へ進み、正常対象は正常として表示できます。ただし監視全体の合否をどう決めるかは別の業務ルールです。allSettledが返ったことを監視成功と誤認しないでください。
反対に、複数の前提条件をすべて満たして初めて実行できる処理ではallが自然です。ただしallの拒否後も既に開始した処理が動き続ける可能性があるため、更新系処理を無造作に並行開始しないことが重要です。読み取り専用の事前確認と、実際の更新・確定を分ける設計が安全です。
この教材の発見は、失敗をなくす方法ではなく、失敗をどの粒度で読者や利用者に見せるかを選べることです。全体の成功を要求する場面と、部分的な結果に価値がある場面を先に決めてからAPIを選ぶと、後から例外処理を継ぎ足すより設計意図が明確になります。
公式情報・一次情報
MDN Promise.all — 拒否時の集約結果
MDN Promise.allSettled — 各結果のstatus、value、reason
Node.js公式ドキュメント — 実行環境の標準API
GitHubサンプル
- Daily-Code-Samples:Promise失敗比較教材 — 完成コード、実行手順、4シナリオのテスト
検証範囲と注意
教材はNode.jsの標準APIを使用します。実ブラウザ、外部通信、注文処理、WordPress表示は確認対象に含めていません。掲載した実行結果は教材の想定出力として読み、各自の環境でテストして確かめてください。
