3MIKAN
仮想通貨直コン

Promise.allが失敗しても処理は止まらない:部分成功・中断・同時実行数を分ける

Promise.allの例外を捕まえた後も処理が続く理由を、手元で動く例から確認します。allSettledによる部分成功、AbortControllerによる中断、開始数を制限する実装を分けて解説します。

3MIKANのキャラクターが独立した作業レーンの停止と継続を見分け、待機中の封筒を管理する場面

複数のデータを同時に取得し、Promise.all()の失敗をcatchで受け取った。それなのに、別の処理から結果が届き続ける。これは「全体が失敗した」という通知と、「開始済みの処理を止める」という操作を混同すると起きます。

この記事は、RPCやAPIの読み取りをまとめるJavaScriptを書いている人向けです。結果をまとめること、部分成功を残すこと、処理を中断すること、同時に開始する数を抑えることを分けます。

例はNode.js v22.16.0で確認しました。通信の代わりに手動で完了させるPromiseとタイマーを使うので、アカウント、APIキー、外部RPCは不要です。各例を.mjsファイルへ保存し、node ファイル名.mjsで実行できます。

失敗を受け取った後にも、別の処理は完了する

Promise.all()が返すのは、渡された複数の結果を一つにまとめたPromiseです。すべて成功すれば結果の配列を返し、一つが失敗すれば、その拒否理由で全体も失敗します。他の処理へ自動的に中断を通知する機能ではありません。[1]

次の例では、処理Bを手動で完了させます。タイマーの速さに依存せず、「全体の失敗を受け取った後にBが続く」ことを確かめるためです。

const events = [];
const gate = Promise.withResolvers();
const taskA = Promise.reject(new Error('A failed'));
const taskB = gate.promise.then(() => {
  events.push('B finished');
  return 'B';
});

try {
  await Promise.all([taskA, taskB]);
} catch {
  events.push('all failed');
}
console.log(events.join(' -> '));

gate.resolve();
await taskB;
console.log(events.join(' -> '));

実行結果は次のとおりです。

all failed
all failed -> B finished

Promise.withResolvers()は、Promiseと、その完了・失敗を外から通知する関数をまとめて作ります。この例では、Bを終わらせる合図だけに使っています。[1]

つまり、catchへ進んだ時点で「Bはもう動かない」と考えることはできません。実際の取得処理なら、後から届いた結果を古い画面へ反映しないか、別の処理と同じ記録を書き換えないかも考える必要があります。

部分成功を残すなら、失敗をゼロへ置き換えない

独立した読み取り結果を一覧に出す場合、どれか一つが失敗したからといって、ほかの成功結果まで捨てる必要はないかもしれません。その判断には、すべての成功・失敗を集めるPromise.allSettled()を使えます。[1]

この関数が返す配列は、完了した順ではなく、入力した順に対応します。次の例では、2番目を先に完了させても、結果は1番目、2番目の順です。

const first = Promise.withResolvers();
const second = Promise.withResolvers();
const pending = Promise.allSettled([first.promise, second.promise]);

second.resolve(0n);
first.reject(new Error('read unavailable'));

const results = await pending;
console.log(results.map((result) =>
  result.status === 'fulfilled'
    ? `ok:${result.value.toString()}`
    : 'unavailable'
).join(', '));
// unavailable, ok:0

ここでは「取得できなかった」と「正常に0を取得した」を残しています。失敗を一律に0nへ置き換えると、その区別がなくなります。

たとえば残高を表示する設計なら、0は取得結果の一つであって、通信に失敗した印ではありません。表示する項目名と入力順の対応も保存し、結果の配列だけを別の順番で並べ替えないようにします。

ただし、allSettled()は全件が決着するまで待ちます。終わらない処理が一つあれば、集約も終わりません。タイムアウトや中断への対応は、別に設計します。[1]

本当に止めるには、処理側も中断に対応する

AbortControllerは、中断の合図を送るための仕組みです。合図を受け取るsignalを、対応するAPIへ渡しておきます。任意のPromiseへ後から強制停止をかける機能ではありません。[3]

Node.jsのPromise版タイマーは、この合図に対応しています。次の例では、Aが失敗したら60秒のタイマーBを中断し、両方の最終状態まで確認します。[2]

import { setTimeout as delay } from 'node:timers/promises';

const controller = new AbortController();
const jobs = [
  Promise.reject(new Error('A failed')),
  delay(60_000, 'B', { signal: controller.signal }),
];

try {
  await Promise.all(jobs);
} catch (error) {
  controller.abort(error);
  const settled = await Promise.allSettled(jobs);
  console.log(settled.map((result) =>
    result.status === 'rejected' ? result.reason.name : result.value
  ).join(', '));
}
// Error, AbortError

この例で確認しているのは、Aの失敗とBのタイマー中断です。60秒待ってから失敗する例ではありません。また、タイマーに渡したsignalを省略すれば、同じabort()を呼んでも、そのタイマーを中断できません。

allSettled()を最後に置くのは、中断を通知した直後に、すべての後始末が完了したと決めつけないためです。ただし、合図を無視する処理まで混ぜれば、そこを待ち続ける可能性は残ります。

外部サービスへ送った要求については、手元で待つのをやめても、相手側の処理や書き込みまで取り消されたとは限りません。中断は、完了済みの変更を巻き戻す操作とは別と考えます。

中断済みのsignalは元へ戻せません。次の独立した処理を始めるときは、新しいAbortControllerを作ります。[3]

同時実行数は、Promiseを作り終わる前に制限する

Promise.all(items.map(readItem))という形では、通常の配列のmap()readItemを各要素について呼び出します。その関数内で取得を開始するなら、Promise.all()へ配列を渡す時点で、すでに開始しています。[4]

開始済みのPromiseを後から2件ずつ待っても、同時に開始した数は減りません。上限を決めたいなら、入力と、これから呼ぶ関数を渡す形にします。

以下は、指定した数だけ処理を動かし、成功・失敗を入力順で返す小さな実装です。1件失敗しても残りを続ける方針で、先ほどの「失敗したら全体を中断する」例とは目的が違います。

async function mapSettledWithLimit(items, limit, worker) {
  if (!Array.isArray(items)) throw new TypeError('items must be an array');
  if (!Number.isSafeInteger(limit) || limit < 1) {
    throw new RangeError('limit must be a positive safe integer');
  }
  if (typeof worker !== 'function') throw new TypeError('worker required');
  for (let i = 0; i < items.length; i += 1) {
    if (!Object.hasOwn(items, i)) throw new TypeError('sparse array rejected');
  }

  const input = items.slice();
  const results = new Array(input.length);
  let cursor = 0;

  async function run() {
    while (cursor < input.length) {
      const index = cursor++;
      try {
        results[index] = {
          status: 'fulfilled',
          value: await worker(input[index], index),
        };
      } catch (reason) {
        results[index] = { status: 'rejected', reason };
      }
    }
  }

  await Promise.all(Array.from(
    { length: Math.min(limit, input.length) },
    () => run(),
  ));
  return results;
}

通常の密な配列を入力とし、上限0、小数の上限、途中に要素がない配列は拒否しています。入力配列の並びは浅くコピーしますが、要素がオブジェクトなら、そのオブジェクトの内容まで複製するわけではありません。

各処理がawaitで待機する前に、担当する位置を一つ確保します。単一のJavaScript実行環境内での制御であり、複数プロセス間の実行数をまとめて制御するものではありません。

上の関数に続けて、次のコードを置きます。通信の代わりに短いタイマーを待ち、一つだけ意図的に失敗させます。

import { setTimeout as delay } from 'node:timers/promises';

let active = 0;
let maximum = 0;
const results = await mapSettledWithLimit([10, 20, 30, 40], 2, async (value) => {
  active += 1;
  maximum = Math.max(maximum, active);
  try {
    await delay(5);
    if (value === 20) throw new Error('sample failure');
    return value * 2;
  } finally {
    active -= 1;
  }
});

console.log(maximum);
console.log(results.map((result) =>
  result.status === 'fulfilled' ? result.value : 'failed'
).join(', '));
// 2
// 20, failed, 60, 80

この例では、同時に実行中の数の最大値が2になり、失敗した要素の後も処理が続きました。数値2は説明用の設定であり、特定のRPCサービスに適した上限や、リクエスト制限を保証する値ではありません。

この関数には、毎秒の要求数制限、再試行、外部からの中断は組み込んでいません。処理がすぐ完了すれば、同時実行数が2でも短時間に多数を開始できます。「同時に2件」と「毎秒2件」は違う条件です。

タイムアウトと、要求の再送も別に考える

Promise.race()で本処理とタイマーを競わせても、勝たなかった側の処理が自動で止まるわけではありません。必要なら、合図に対応する処理へ中断を伝え、不要になったタイマーも片付けます。[1][2]

読み取りの再試行を設計するときも、入力、対象ブロック、試行回数を管理します。現在の値を毎回取り直すなら、同じ入力でも結果が変わる可能性があるためです。固定ブロックでの読み取りは、RPCのブロック指定を整理した記事を参照してください。

送信や書き込みでは、応答が得られなかったことだけを理由に再送しない設計が必要です。待機の失敗と、相手が要求を受理していないことは同じではありません。この記事の中断例を、送信済み取引の取り消しへ流用しないでください。

まとめ:四つの判断を一つにしない

決めたいこと この記事で使ったもの それだけでは決まらないこと
全件成功したか Promise.all() 開始済み処理の中断
どれが成功し、どれが失敗したか Promise.allSettled() 終わらない処理の終了
対応する処理へ中断を伝える AbortController 完了済み変更の巻き戻し
同時に開始・実行する数を抑える 開始前に呼び出しを制御する関数 毎秒の制限、再試行方針

最初に決めるのは「全体の成功が必要か、それとも部分成功を残すか」です。そのうえで、中断に対応する処理か、開始数を制限する必要があるかを別々に確認します。

一次情報

確認日:2026年9月6日。表示している版と、手元で実行したNode.jsの版は区別しています。

[1] ECMAScript 2025 — Promise.all / allSettled / race / withResolvers

[2] Node.js — Cancelling timers / Timers Promises API

[3] Node.js 22.16.0 — AbortController / AbortSignal

[4] ECMAScript 2025 — Array.prototype.map

確認した一次情報