- 技術 💻
Promise.all・allSettled・race・anyの違い|実行結果で比較
Promise.all は「全部成功したら成功、1つでも失敗したらその時点で失敗」、Promise.allSettled は「成功・失敗を問わず全部の結果を待つ」、Promise.race は「最初に決着した1つ(成功でも失敗でも)を採用」、Promise.any は「最初に成功した1つを採用し、全部失敗したときだけ AggregateError で失敗」する、複数の Promise をまとめて扱うための静的メソッドです。 4つとも配列(反復可能なもの)を受け取り、1つの Promise を返す点は同じで、違いは「いつ決着するか」と「何を返すか」だけです。
この記事では、4メソッドを「全成功/一部失敗/全失敗/失敗が先に来る」の4ケースで Node.js 22 で実際に実行し、戻り値・例外(AggregateError の中身)・所要時間を表にまとめました。 そのうえで、タイムアウト、フォールバック、失敗した分だけの再試行、同時実行数の制限、順序を保った並列処理、Promise.withResolvers と Promise.try といった実務パターンを、コピペして動く形で載せています。最後に、catch のない失敗でプロセスが止まる、先に始めて順に await すると失敗が「未処理」になる、といったつまずきも実際の出力で確認します。
Promise.all・allSettled・race・any の早見表
| メソッド | 成功(履行)する条件 | 失敗(拒否)する条件 | 戻り値の形 | 決着のタイミング | 主な用途 |
|---|---|---|---|---|---|
Promise.all |
全部が成功 | 1つでも失敗(最初の失敗) | 値の配列(渡した順) | 全部成功なら最後の完了時、失敗なら最初の失敗時 | 全部そろって初めて意味がある処理(画面の初期データをまとめて取得など) |
Promise.allSettled |
常に成功 | しない | { status, value | reason } の配列(渡した順) |
全部の決着後 | 一部が失敗しても続けたい、失敗した分だけやり直したい |
Promise.race |
最初に決着したものが成功 | 最初に決着したものが失敗 | 最初に決着した1つの値 | 最初の決着時 | タイムアウト、「どちらか早いほう」 |
Promise.any |
最初に成功したものが成功 | 全部失敗 | 最初に成功した1つの値/AggregateError |
最初の成功時(全滅なら最後の失敗時) | 複数の取得先のうち、使えるものを1つ選ぶ(フォールバック) |
4ケースを実行した結果の要約(詳しい出力は本文)。A・B・C は 100・200・300ms 後に決着する処理です。
| ケース | all |
allSettled |
race |
any |
|---|---|---|---|---|
| 全成功 | ["A","B","C"](300ms) |
3件とも fulfilled(300ms) | "A"(100ms) |
"A"(100ms) |
| 一部失敗(C が失敗) | 拒否 C failed(300ms) |
A・B fulfilled、C rejected(300ms) | "A"(100ms) |
"A"(100ms) |
| 全失敗 | 拒否 A failed(100ms) |
3件とも rejected(300ms) | 拒否 A failed(100ms) |
拒否 AggregateError(300ms) |
| 失敗が先(A が失敗) | 拒否 A failed(100ms) |
A rejected、B・C fulfilled(300ms) | 拒否 A failed(100ms) |
"B"(200ms) |
対応状況(@mdn/browser-compat-data 8.1.3 より)
| 機能 | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
Promise.all / Promise.race |
32 | 29 | 8 | 0.12 |
Promise.allSettled |
76 | 71 | 13 | 12.9 |
Promise.any / AggregateError |
85 | 79 | 14 | 15.0 |
Promise.withResolvers |
119 | 121 | 17.4 | 22.0 |
Promise.try |
128 | 134 | 18.2 | 23.0 |
Array.fromAsync |
121 | 115 | 16.4 | 22.0 |
AbortSignal.timeout |
124 | 100 | 16 | 17.3 |
AbortSignal.any |
116 | 124 | 17.4 | 20.3 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Promise.all・allSettled・race・anyの違い|実行結果で比較」
検証環境
| 項目 | 内容 |
|---|---|
| Node.js | v22.22.2(V8 12.4) |
| ブラウザ | Chromium 141.0.7390.37(Playwright 1.56.0、Promise.try の確認に使用) |
| 対応状況の確認 | @mdn/browser-compat-data 8.1.3、MDN |
| 実行方法 | ESM(.mjs)でトップレベル await を使用。node ファイル名.mjs |
記事中の実行結果はこの環境の出力をそのまま載せています。所要時間は performance.now() で計測したもので、数ms の誤差があります。
4ケース × 4メソッドを実際に動かす
まず、指定ミリ秒後に成功または失敗する wait 関数を用意し、4つのケースを4つのメソッドすべてに通します。 この1本を動かすだけで、4メソッドの違いが一度に確認できます。
// p1_matrix.mjs(Node 22 / ESM)
const wait = (ms, value, fail = false) =>
new Promise((resolve, reject) =>
setTimeout(() => (fail ? reject(new Error(`${value} failed`)) : resolve(value)), ms));
const cases = {
"全成功": () => [wait(100, "A"), wait(200, "B"), wait(300, "C")],
"一部失敗(最後)": () => [wait(100, "A"), wait(200, "B"), wait(300, "C", true)],
"全失敗": () => [wait(100, "A", true), wait(200, "B", true), wait(300, "C", true)],
"失敗が先": () => [wait(100, "A", true), wait(200, "B"), wait(300, "C")],
};
const methods = ["all", "allSettled", "race", "any"];
const show = (v) => {
if (v instanceof AggregateError) return `AggregateError(${JSON.stringify(v.message)}, errors=[${v.errors.map(e => e.message).join(", ")}])`;
if (v instanceof Error) return `Error(${JSON.stringify(v.message)})`;
if (Array.isArray(v)) return `[${v.map(x => x && x.status ? (x.status === "fulfilled" ? `{fulfilled: ${JSON.stringify(x.value)}}` : `{rejected: ${show(x.reason)}}`) : JSON.stringify(x)).join(", ")}]`;
return JSON.stringify(v);
};
for (const [caseName, make] of Object.entries(cases)) {
console.log(`\n[${caseName}]`);
for (const m of methods) {
const start = performance.now();
let outcome;
try {
const value = await Promise[m](make());
outcome = `fulfilled → ${show(value)}`;
} catch (err) {
outcome = `rejected → ${show(err)}`;
}
const ms = Math.round(performance.now() - start);
console.log(`Promise.${m.padEnd(10)} ${String(ms).padStart(3)}ms ${outcome}`);
}
}
実行結果(Node v22.22.2):
[全成功]
Promise.all 300ms fulfilled → ["A", "B", "C"]
Promise.allSettled 300ms fulfilled → [{fulfilled: "A"}, {fulfilled: "B"}, {fulfilled: "C"}]
Promise.race 101ms fulfilled → "A"
Promise.any 101ms fulfilled → "A"
[一部失敗(最後)]
Promise.all 301ms rejected → Error("C failed")
Promise.allSettled 301ms fulfilled → [{fulfilled: "A"}, {fulfilled: "B"}, {rejected: Error("C failed")}]
Promise.race 100ms fulfilled → "A"
Promise.any 101ms fulfilled → "A"
[全失敗]
Promise.all 100ms rejected → Error("A failed")
Promise.allSettled 300ms fulfilled → [{rejected: Error("A failed")}, {rejected: Error("B failed")}, {rejected: Error("C failed")}]
Promise.race 101ms rejected → Error("A failed")
Promise.any 300ms rejected → AggregateError("All promises were rejected", errors=[A failed, B failed, C failed])
[失敗が先]
Promise.all 100ms rejected → Error("A failed")
Promise.allSettled 300ms fulfilled → [{rejected: Error("A failed")}, {fulfilled: "B"}, {fulfilled: "C"}]
Promise.race 100ms rejected → Error("A failed")
Promise.any 200ms fulfilled → "B"
この結果から読み取れるポイントは次の4つです。
| 観点 | わかること |
|---|---|
| 所要時間 | all と any は「途中で結論が出た時点」で終わる(全失敗の all は 100ms、失敗が先の any は 200ms)。allSettled は常に最後の決着(300ms)まで待つ |
| 失敗の扱い | all は最初の失敗をそのまま投げる。any は全滅したときだけ、全部の失敗を errors に束ねた AggregateError を投げる |
race |
成功・失敗を区別せず「最初に決着したもの」を返す。失敗が先に来ればそのまま失敗になる |
| 戻り値の順序 | all と allSettled の配列は、完了した順ではなく渡した順 |
AggregateError の中身と、空配列・Promise 以外の値
Promise.any が全滅したときの AggregateError は、errors プロパティに各 Promise の失敗理由を「渡した順」で持ちます。 message は "All promises were rejected" で固定です。空配列を渡したときの挙動もメソッドごとに違うので、あわせて確認します。
// p2_aggregate.mjs
const wait = (ms, value, fail = false) =>
new Promise((resolve, reject) =>
setTimeout(() => (fail ? reject(new Error(`${value} failed`)) : resolve(value)), ms));
try {
await Promise.any([wait(300, "A", true), wait(100, "B", true), wait(200, "C", true)]);
} catch (err) {
console.log("name :", err.name);
console.log("message :", err.message);
console.log("errors :", err.errors.map((e) => e.message)); // 入力順(失敗した順ではない)
console.log("instanceof AggregateError:", err instanceof AggregateError);
console.log("instanceof Error :", err instanceof Error);
}
console.log("all([]) :", await Promise.all([]));
console.log("allSettled([]) :", await Promise.allSettled([]));
console.log("any([]) :", await Promise.any([]).catch((e) => `${e.name}: ${e.message}`));
const raceEmpty = Promise.race([]);
const result = await Promise.race([raceEmpty, wait(50, "timeout")]);
console.log("race([]) :", result === "timeout" ? "決着しない(永遠に pending)" : result);
console.log("all([1, p, 'x']):", await Promise.all([1, wait(10, 2), "x"]));
console.log("race([3, p]) :", await Promise.race([3, wait(10, 4)]));
name : AggregateError
message : All promises were rejected
errors : [ 'A failed', 'B failed', 'C failed' ]
instanceof AggregateError: true
instanceof Error : true
all([]) : []
allSettled([]) : []
any([]) : AggregateError: All promises were rejected
race([]) : 決着しない(永遠に pending)
all([1, p, 'x']): [ 1, 2, 'x' ]
race([3, p]) : 3
| 入力 | all |
allSettled |
race |
any |
|---|---|---|---|---|
空配列 [] |
[] で即成功 |
[] で即成功 |
永遠に決着しない | AggregateError で即失敗 |
| Promise でない値 | そのまま値として扱う | {status:"fulfilled"} になる |
即座にその値で成功 | 即座にその値で成功 |
Promise.race([]) は永遠に pending のままなので、動的に作った配列を渡すときは空でないことを確かめておくと安心です。
どれを使うかの選び方
判断の軸は「全部必要か、1つでよいか」と「失敗をどう扱いたいか」の2つです。
flowchart TD
Q1{結果は全部必要?} -- はい --> Q2{1つでも失敗したら<br>全体を失敗にしたい?}
Q1 -- 1つでよい --> Q3{失敗したものは無視して<br>成功を待ちたい?}
Q2 -- はい --> ALL[Promise.all]
Q2 -- いいえ、失敗した分も個別に扱う --> SETTLED[Promise.allSettled]
Q3 -- はい --> ANY[Promise.any]
Q3 -- いいえ、最初の決着で決める --> RACE[Promise.race]
| やりたいこと | 使うもの | 理由 |
|---|---|---|
| 画面の初期表示に必要なデータを3つまとめて取る | Promise.all |
1つでも欠けると表示できないので、早く失敗を知りたい |
| 100件のタスクを一括処理し、失敗した分だけ報告・再試行する | Promise.allSettled |
途中の失敗で全体を止めたくない |
| 処理に時間の上限を付ける | Promise.race(+タイマー)または AbortSignal.timeout |
最初に決着した側を採用すればよい |
| メインとミラーの取得先から、応答があったほうを使う | Promise.any |
失敗した取得先は無視して、成功を1つ選びたい |
| 順番に依存する処理(前の結果を次に使う) | どれも使わず for...of + await |
並列にできない |
実務パターン:コピペで使えるコードと実行結果
タイムアウトを付ける(race と AbortSignal)
どんな Promise にも付けられる汎用のタイムアウトは Promise.race で作れます。ただし、負けた側の処理は止まらないので、処理自体を止めたいときは AbortSignal.timeout を渡します。 Promise.race 版では、勝敗が決まったあとにタイマーが残らないよう finally で clearTimeout します。
// p3_timeout.mjs
import { setTimeout as sleep } from "node:timers/promises";
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
// 1) どんな Promise にも付けられる汎用タイムアウト(Promise.race)
function withTimeout(promise, ms, message = `Timeout after ${ms}ms`) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); // タイマーを必ず片付ける
}
console.log("1a:", await withTimeout(wait(100, "間に合った"), 500));
console.log("1b:", await withTimeout(wait(1000, "遅い"), 200).catch((e) => `${e.name}: ${e.message}`));
// 2) signal を受け取れる処理には AbortSignal.timeout(処理そのものが止まる)
try {
await sleep(1000, "done", { signal: AbortSignal.timeout(200) });
} catch (e) {
console.log("2 :", e.name, "-", e.message, "| cause:", e.cause?.name, "-", e.cause?.message);
}
// 3) ユーザーの中断とタイムアウトを1つの signal にまとめる(AbortSignal.any)
const controller = new AbortController();
setTimeout(() => controller.abort(new Error("ユーザーがキャンセル")), 100);
const combined = AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]);
try {
await sleep(1000, "done", { signal: combined });
} catch (e) {
console.log("3 :", e.name, "-", e.message, "| cause:", e.cause?.message, "| signal.reason:", combined.reason?.message);
}
1a: 間に合った
1b: Error: Timeout after 200ms
2 : AbortError - The operation was aborted | cause: TimeoutError - The operation was aborted due to timeout
3 : AbortError - The operation was aborted | cause: ユーザーがキャンセル | signal.reason: ユーザーがキャンセル
このスクリプト全体の実行時間は約 1.1 秒でした。 1b でタイムアウトした wait(1000) は race に負けたあとも動き続け、そのタイマーが終わるまでプロセスが残るためです。fetch や timers/promises のように signal を受け取れる処理なら、2・3 の形で処理そのものを止められます。timers/promises の場合、エラーは AbortError で、時間切れだったのかユーザー操作だったのかは cause(または signal.reason)で判別できます。
| 方法 | 対象 | 負けた側の処理 | 判別 |
|---|---|---|---|
Promise.race + タイマー |
どんな Promise でも | 止まらない(完了まで動く) | 自分で投げたエラー |
AbortSignal.timeout(ms) |
signal を受け取れる処理 |
止まる | TimeoutError(fetch)/AbortError + cause(timers/promises) |
AbortSignal.any([...]) |
同上 | 止まる | signal.reason に最初の理由が入る |
フォールバック:使える取得先を1つ選ぶ(any)
Promise.any は「全部に同時に問い合わせて、最初に成功したものを使う」動きです。 速い反面、すべての取得先に負荷がかかるので、「メインが失敗したときだけミラーに行く」なら順番に試す関数のほうが向きます。どちらも全滅したときは AggregateError に理由をまとめられます。
// p4_fallback.mjs
const wait = (ms, value, fail = false) =>
new Promise((resolve, reject) =>
setTimeout(() => (fail ? reject(new Error(`${value}: 取得できませんでした`)) : resolve(`${value} のデータ`)), ms));
const log = [];
const source = (name, ms, fail = false) => { log.push(`${name} 開始`); return wait(ms, name, fail); };
// 1) 同時に問い合わせて、最初に成功したものを使う
const t0 = performance.now();
const value = await Promise.any([source("メイン", 300, true), source("ミラー1", 200), source("ミラー2", 100, true)]);
console.log("any :", value, `(${Math.round(performance.now() - t0)}ms)`, "| 開始したもの:", log.join(", "));
// 2) 順番に試して、成功したら止める
async function firstSuccess(factories) {
const errors = [];
for (const make of factories) {
try { return await make(); } catch (e) { errors.push(e); }
}
throw new AggregateError(errors, "すべての取得先で失敗しました");
}
log.length = 0;
const t1 = performance.now();
const v2 = await firstSuccess([() => source("メイン", 300, true), () => source("ミラー1", 200), () => source("ミラー2", 100, true)]);
console.log("順番 :", v2, `(${Math.round(performance.now() - t1)}ms)`, "| 開始したもの:", log.join(", "));
// 3) 全部失敗したときの扱い
try {
await Promise.any([source("メイン", 50, true), source("ミラー1", 50, true)]);
} catch (e) {
console.log("全滅 :", e.constructor.name, "|", e.errors.map((x) => x.message));
}
any : ミラー1 のデータ (201ms) | 開始したもの: メイン 開始, ミラー1 開始, ミラー2 開始
順番 : ミラー1 のデータ (500ms) | 開始したもの: メイン 開始, ミラー1 開始
全滅 : AggregateError | [ 'メイン: 取得できませんでした', 'ミラー1: 取得できませんでした' ]
AggregateError は自分でも new AggregateError(errors配列, message) で作れるので、順番に試す版でも「何が失敗したか」を同じ形で呼び出し側に返せます。
失敗した分だけ再試行する(allSettled)
allSettled の結果から rejected だけを取り出して再実行すれば、成功した分をやり直さずに済みます。 元の配列での位置(index)を一緒に持ち回ることで、結果の順序も保てます。
// p5_retry.mjs
const attempts = new Map(); // id → 何回目か
// 2回目までは失敗する id(3 と 5)を含む、架空の「注文を取り込む」処理
async function importOrder(id) {
const n = (attempts.get(id) ?? 0) + 1;
attempts.set(id, n);
await new Promise((r) => setTimeout(r, 30));
if ((id === 3 || id === 5) && n < 3) throw new Error(`order ${id}: 一時的に失敗 (${n}回目)`);
return { id, status: "imported", tries: n };
}
async function allWithRetry(ids, task, { retries = 2, delayMs = 100 } = {}) {
const results = new Array(ids.length);
let pending = ids.map((id, index) => ({ id, index }));
for (let round = 0; round <= retries && pending.length > 0; round++) {
if (round > 0) {
console.log(` 再試行 ${round}回目 → 対象: ${pending.map((p) => p.id).join(", ")}`);
await new Promise((r) => setTimeout(r, delayMs * round)); // 待ち時間を回数に応じて伸ばす
}
const settled = await Promise.allSettled(pending.map((p) => task(p.id)));
const next = [];
settled.forEach((s, i) => {
const p = pending[i];
if (s.status === "fulfilled") results[p.index] = { ok: true, value: s.value };
else { results[p.index] = { ok: false, error: s.reason }; next.push(p); }
});
pending = next;
}
return results;
}
const results = await allWithRetry([1, 2, 3, 4, 5], importOrder, { retries: 2 });
for (const r of results) console.log(r.ok ? ` OK ${JSON.stringify(r.value)}` : ` NG ${r.error.message}`);
再試行 1回目 → 対象: 3, 5
再試行 2回目 → 対象: 3, 5
OK {"id":1,"status":"imported","tries":1}
OK {"id":2,"status":"imported","tries":1}
OK {"id":3,"status":"imported","tries":3}
OK {"id":4,"status":"imported","tries":1}
OK {"id":5,"status":"imported","tries":3}
再試行の上限を超えても失敗したものは ok: false のまま返るので、呼び出し側で「失敗一覧」として扱えます。
同時実行数を制限する(自作 pLimit と Array.fromAsync)
Promise.all(items.map(fn)) は全件を一度に開始します。 件数が多いときは、同時に動かす数を制限する関数を挟みます。ここでは pLimit 風の関数を自作し、Array.fromAsync と for...of の直列と比べました。
// p6_limit.mjs
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
let running = 0, peak = 0;
async function task(id) {
running++; peak = Math.max(peak, running);
await wait(100, id);
running--;
return `item${id}`;
}
const ids = [1, 2, 3, 4, 5, 6];
async function measure(label, fn) {
running = 0; peak = 0;
const t = performance.now();
const out = await fn();
console.log(`${label.padEnd(26)} ${String(Math.round(performance.now() - t)).padStart(4)}ms 同時最大=${peak} ${JSON.stringify(out)}`);
}
// 1) 制限なし:全部同時
await measure("Promise.all(map)", () => Promise.all(ids.map(task)));
// 2) 自作 pLimit(キュー方式。順序を保って最大 n 件ずつ)
function pLimit(concurrency) {
const queue = [];
let active = 0;
const next = () => {
if (active >= concurrency || queue.length === 0) return;
active++;
const { fn, resolve, reject } = queue.shift();
fn().then(resolve, reject).finally(() => { active--; next(); });
};
return (fn) => new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); next(); });
}
const limit = pLimit(2);
await measure("pLimit(2)", () => Promise.all(ids.map((id) => limit(() => task(id)))));
// 3) Array.fromAsync:1件ずつ順番に(直列)
await measure("Array.fromAsync", () => Array.fromAsync(ids, task));
// 4) for...of + await も直列(比較用)
await measure("for...of + await", async () => { const out = []; for (const id of ids) out.push(await task(id)); return out; });
Promise.all(map) 101ms 同時最大=6 ["item1","item2","item3","item4","item5","item6"]
pLimit(2) 302ms 同時最大=2 ["item1","item2","item3","item4","item5","item6"]
Array.fromAsync 602ms 同時最大=1 ["item1","item2","item3","item4","item5","item6"]
for...of + await 602ms 同時最大=1 ["item1","item2","item3","item4","item5","item6"]
| 方法 | 同時実行数 | 所要時間(100ms × 6件) | 向いている場面 |
|---|---|---|---|
Promise.all(map) |
無制限(6) | 約100ms | 件数が少ない、相手に負荷の上限がない |
pLimit(n) |
n | 約 100 × ⌈6 ÷ n⌉ ms(n=2 で約300ms) | 件数が多い、相手の同時接続数に上限がある |
Array.fromAsync(items, fn) |
1(直列) | 約600ms | 1件ずつ順に処理したい、非同期イテレーターを配列にしたい |
for...of + await |
1(直列) | 約600ms | 前の結果を次に使う |
Array.fromAsync は非同期の反復処理を「1件ずつ待ちながら」配列にするメソッドで、for await...of と同じく直列です。並列にしたいときは Promise.all(map) か上の制限付きの関数を使います。ライブラリでは p-limit(npm)が同じ役割を担います。
順序を保った並列処理(完了順との違い)
Promise.all の結果は「完了した順」ではなく「渡した順」に並びます。 一方、then の中で処理を書けば「完了した順」に処理できます。
// p7_order.mjs(抜粋)
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const jobs = [["A", 300], ["B", 100], ["C", 200]];
const finishedOrder = [];
const results = await Promise.all(
jobs.map(([name, ms]) => wait(ms, name).then((v) => { finishedOrder.push(v); return v; }))
);
console.log("完了した順 :", finishedOrder);
console.log("all の結果 :", results);
const order2 = [];
await Promise.all(jobs.map(([name, ms]) => wait(ms, name).then((v) => order2.push(`${v} を処理`))));
console.log("完了順に処理:", order2);
完了した順 : [ 'B', 'C', 'A' ]
all の結果 : [ 'A', 'B', 'C' ]
完了順に処理: [ 'B を処理', 'C を処理', 'A を処理' ]
Promise.withResolvers:外から resolve できる Promise を作る
Promise.withResolvers() は { promise, resolve, reject } を返し、new Promise のコールバックの外で resolve を呼べるようにします。 イベントやコールバック形式の API を Promise に変換するときや、「合図が来るまで複数の処理を待たせるゲート」を作るときに、変数のスコープを工夫する必要がなくなります(Node 22.0、Chrome 119、Firefox 121、Safari 17.4 以降)。
// p7_order.mjs(続き)
import { EventEmitter } from "node:events";
// 1) イベントを Promise に変換する
const { promise, resolve, reject } = Promise.withResolvers();
const emitter = new EventEmitter();
emitter.once("done", (v) => resolve(v));
emitter.once("error", (e) => reject(e));
setTimeout(() => emitter.emit("done", "イベントで完了"), 50);
console.log("withResolvers:", await promise);
// 2) 「開始の合図を待つ」ゲート
const gate = Promise.withResolvers();
const workers = ["w1", "w2", "w3"].map(async (name) => { await gate.promise; return `${name} 開始`; });
setTimeout(() => gate.resolve(), 30); // 合図を送るまで全員待機
console.log("ゲート :", await Promise.all(workers));
withResolvers: イベントで完了
ゲート : [ 'w1 開始', 'w2 開始', 'w3 開始' ]
Promise.try:同期関数の throw も rejected Promise にそろえる
Promise.try(fn, ...args) は、fn を同期的に呼び、返り値を Promise に包みます。同期的に throw しても rejected な Promise になるので、「同期かもしれないし非同期かもしれない関数」を Promise の流れに乗せるときに try/catch を書かずに済みます。Node 22 には入っていない(Node 23 以降、Chrome 128、Firefox 134、Safari 18.2 以降)ため、Node 22 では代替の1行を使い、Chromium 141 で本物の動きを確認しました。
// p8_try.mjs(抜粋)
console.log("typeof Promise.try:", typeof Promise.try);
try { Promise.try(() => 1); } catch (e) { console.log(`${e.name}: ${e.message}`); }
// Node 22 向けの代替(同じ動きをする1行)
const promiseTry = (fn, ...args) => new Promise((resolve) => resolve(fn(...args)));
console.log("代替:", await promiseTry(() => 21 * 2), "/", await promiseTry(() => { throw new Error("NG"); }).catch((e) => e.message));
--- Node v22.22.2
typeof Promise.try: undefined
TypeError: Promise.try is not a function
代替: 42 / NG
Chromium 141(Playwright の page.evaluate で実行):
const risky = (n) => { if (n < 0) throw new Error("負の値は扱えません"); return n * 2; };
await Promise.try(risky, 21).then((v) => console.log("try(risky, 21) → fulfilled:", v));
await Promise.try(risky, -1).catch((e) => console.log("try(risky, -1) → rejected :", e.message));
await Promise.try(async () => "async OK").then((v) => console.log("try(async fn) → fulfilled:", v));
--- Chromium 141.0.7390.37
try(risky, 21) → fulfilled: 42
try(risky, -1) → rejected : 負の値は扱えません
try(async fn) → fulfilled: async OK
typeof Promise.try: function
new Promise((resolve) => resolve(fn())) との違いは、fn が返した Promise をそのまま採用する(二重に包まない)点と、引数を Promise.try(fn, a, b) の形で渡せる点です。
よくあるつまずきと直し方
1. catch のない失敗でプロセスが止まる
Promise.all の結果を await も catch もせずに放置すると、1つでも失敗した時点で「未処理の拒否(unhandled rejection)」となり、Node.js は既定でプロセスを終了します。
// p9a_unhandled.mjs
const wait = (ms, value, fail = false) =>
new Promise((resolve, reject) =>
setTimeout(() => (fail ? reject(new Error(`${value} failed`)) : resolve(value)), ms));
Promise.all([wait(100, "A"), wait(200, "B", true)]); // await も catch もしていない
console.log("all を呼び出した直後(まだ動いている)");
setTimeout(() => console.log("500ms 後:ここは表示されない"), 500);
all を呼び出した直後(まだ動いている)
file:///.../p9a_unhandled.mjs:4
setTimeout(() => (fail ? reject(new Error(`${value} failed`)) : resolve(value)), ms));
^
Error: B failed
at Timeout._onTimeout (file:///.../p9a_unhandled.mjs:4:37)
at listOnTimeout (node:internal/timers:585:17)
at process.processTimers (node:internal/timers:521:7)
Node.js v22.22.2
終了コードは 1 で、500ms 後の行は表示されません。直し方は「返ってきた Promise を必ず await して try/catch するか、.catch() を付ける」です。結果を使わない「投げっぱなし」の処理でも .catch((e) => log(e)) は付けておきます。
2. 先に始めてから順番に await すると、後ろの失敗が「未処理」になる
「並列にしたいので先に呼び出しておき、あとで順番に await する」書き方は、後ろの Promise が先に失敗すると、await が届く前に未処理の拒否と判定されます。 これは見落としやすいつまずきです。
// p9b_sequential_await.mjs
const wait = (ms, value, fail = false) =>
new Promise((resolve, reject) =>
setTimeout(() => (fail ? reject(new Error(`${value} failed`)) : resolve(value)), ms));
process.on("unhandledRejection", (reason) => {
console.log("unhandledRejection が発生:", reason.message);
});
const pa = wait(300, "A"); // 300ms 後に成功
const pb = wait(100, "B", true); // 100ms 後に失敗(まだ誰も待っていない)
try {
const a = await pa; // ここで 300ms 待つ間に pb が失敗する
const b = await pb; // ここに来る前に「未処理の拒否」と判定される
console.log(a, b);
} catch (e) {
console.log("catch:", e.message);
}
// 直した形:Promise.all で「まとめて」待つ
try {
const [a, b] = await Promise.all([wait(300, "A"), wait(100, "B", true)]);
console.log(a, b);
} catch (e) {
console.log("catch (all):", e.message);
}
unhandledRejection が発生: B failed
catch: B failed
(node:7860) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
(Use `node --trace-warnings ...` to show where the warning was created)
catch (all): B failed
unhandledRejection のハンドラを外して既定の設定で動かすと、1つ目の await pa の途中(100ms 時点)で Error: B failed によりプロセスが終了し、終了コードは 1 でした。直し方は、先に作った Promise は Promise.all(または allSettled)で「まとめて」待つことです。all は渡された全部の Promise に内部でハンドラを付けるので、どれが先に失敗しても未処理にはなりません。
3. all が失敗しても、残りの処理は最後まで動く
Promise.all が最初の失敗で決着しても、他の Promise はキャンセルされず、副作用(保存・送信など)はそのまま発生します。
// p9c_continue.mjs
const t0 = performance.now();
const stamp = () => `${String(Math.round(performance.now() - t0)).padStart(3)}ms`;
async function saveItem(name, ms, fail = false) {
await new Promise((r) => setTimeout(r, ms));
if (fail) throw new Error(`${name} の保存に失敗`);
console.log(`${stamp()} ${name} を保存しました(副作用はここで発生)`);
return name;
}
try {
await Promise.all([saveItem("item1", 100), saveItem("item2", 200, true), saveItem("item3", 300)]);
} catch (e) {
console.log(`${stamp()} catch: ${e.message} ← all はここで終わるが…`);
}
await new Promise((r) => setTimeout(r, 200));
console.log(`${stamp()} 終了`);
101ms item1 を保存しました(副作用はここで発生)
200ms catch: item2 の保存に失敗 ← all はここで終わるが…
300ms item3 を保存しました(副作用はここで発生)
401ms 終了
「1つ失敗したら残りも止めたい」場合は、AbortController を1つ用意して全員に signal を渡し、誰かが失敗した時点で abort() します。
// p9e_abort_all.mjs
import { setTimeout as sleep } from "node:timers/promises";
const t0 = performance.now();
const stamp = () => `${String(Math.round(performance.now() - t0)).padStart(3)}ms`;
async function saveItem(name, ms, { signal, fail = false } = {}) {
await sleep(ms, undefined, { signal }); // signal を受け取れる処理なら途中で止まる
if (fail) throw new Error(`${name} の保存に失敗`);
console.log(`${stamp()} ${name} を保存しました`);
return name;
}
async function allOrAbort(factories) {
const controller = new AbortController();
const wrapped = factories.map((make) =>
make(controller.signal).catch((e) => { controller.abort(e); throw e; }) // 誰かが失敗したら全員に中断を伝える
);
return Promise.all(wrapped);
}
try {
await allOrAbort([
(signal) => saveItem("item1", 100, { signal }),
(signal) => saveItem("item2", 200, { signal, fail: true }),
(signal) => saveItem("item3", 300, { signal }),
]);
} catch (e) {
console.log(`${stamp()} catch: ${e.message}`);
}
await sleep(200);
console.log(`${stamp()} 終了(item3 は保存されていない)`);
101ms item1 を保存しました
202ms catch: item2 の保存に失敗
403ms 終了(item3 は保存されていない)
signal を受け取れない処理(自前の setTimeout など)は止められないので、その場合は処理の側で signal.aborted を確認する分岐を入れます。
4. forEach の async は待たれない/await を並べると直列になる
forEach に渡した async 関数の Promise は誰も待たないので、forEach の直後に処理が進みます。また、await を縦に並べると1つずつ順番に実行され、時間は足し算になります。
// p9d_foreach.mjs
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
const t0 = performance.now();
const stamp = () => `${String(Math.round(performance.now() - t0)).padStart(3)}ms`;
[1, 2, 3].forEach(async (id) => { await wait(100); console.log(`${stamp()} forEach: ${id} 完了`); });
console.log(`${stamp()} forEach の直後(全部完了する前に進む)`);
await wait(150);
const t1 = performance.now();
const a = await wait(100, "a"); const b = await wait(100, "b"); const c = await wait(100, "c");
console.log(`直列: ${Math.round(performance.now() - t1)}ms`, [a, b, c]);
const t2 = performance.now();
const all = await Promise.all([wait(100, "a"), wait(100, "b"), wait(100, "c")]);
console.log(`並列: ${Math.round(performance.now() - t2)}ms`, all);
0ms forEach の直後(全部完了する前に進む)
101ms forEach: 1 完了
101ms forEach: 2 完了
101ms forEach: 3 完了
直列: 302ms [ 'a', 'b', 'c' ]
並列: 100ms [ 'a', 'b', 'c' ]
| 書き方 | 結果 | こうするとよい |
|---|---|---|
arr.forEach(async ...) |
完了を待たずに次へ進む | await Promise.all(arr.map(async ...))、順番が必要なら for...of |
await f(); await g(); |
直列(足し算) | 互いに依存しないなら await Promise.all([f(), g()]) |
const p = f(); const q = g(); await p; await q; |
後ろが先に失敗すると未処理の拒否 | await Promise.all([p, q]) |
つまずきのチェックリスト
| 確認すること | 理由 |
|---|---|
返ってきた Promise に await か .catch() が付いているか |
未処理の拒否でプロセスが終了する |
| 先に作った Promise を「まとめて」待っているか | 順番に await すると後ろの失敗が未処理になる |
all の失敗時に残りの処理が動き続けてよいか |
止めたいなら AbortSignal を渡す |
race に渡す配列が空になりうるか |
空だと永遠に決着しない |
race のタイマーを clearTimeout しているか |
勝敗が決まったあとも待ち続ける |
| 同時実行数の上限が必要か | 全件同時に開始すると相手側に負荷が集中する |
よくある質問
Q. Promise.all と Promise.allSettled はどう使い分けますか?
A. 「1つでも欠けたら意味がない」なら all、「できた分だけでも使いたい」「失敗した分を報告・再試行したい」なら allSettled です。allSettled は常に成功するので、status を見て fulfilled と rejected を振り分ける処理が必要になります。
Q. Promise.race と Promise.any の違いは何ですか?
A. race は成功・失敗を問わず最初に決着したものを返し、any は最初に成功したものを返します。 失敗が先に来た場合、race はその失敗で終わり、any は次の成功を待ちます。any が失敗するのは全部失敗したときだけで、そのとき AggregateError の errors に各理由が渡した順で入ります。
Q. Promise.all で1つ失敗したとき、他の処理をキャンセルできますか?
A. Promise 自体にキャンセルの仕組みはないので、AbortController の signal を各処理に渡し、失敗した時点で abort() を呼びます。 fetch や timers/promises のように signal を受け取れる処理なら途中で止まります。受け取れない処理は、処理の側で signal.aborted を確認して抜けるようにします。
Q. Promise.try と Promise.withResolvers は今すぐ使えますか?
A. Promise.withResolvers は Node 22・Chrome 119・Firefox 121・Safari 17.4 以降で使えます。Promise.try は Node 23・Chrome 128・Firefox 134・Safari 18.2 以降で、Node 22 では TypeError: Promise.try is not a function になります。 Node 22 で同じ動きが必要なら、記事中の new Promise((resolve) => resolve(fn(...args))) の1行で代替できます。
まとめ
allは全部成功で成功・1つでも失敗なら即失敗、allSettledは常に全部の結果を返す、raceは最初の決着、anyは最初の成功- 所要時間は「結論が出た時点」で決まる。
allの失敗とanyの成功は途中で返り、allSettledは必ず最後まで待つ anyの全滅はAggregateError。errorsに各失敗が渡した順で入る。race([])は永遠に決着しない- タイムアウトは
race+ タイマー(負けた側は止まらない)かAbortSignal.timeout(処理が止まる)。中断と両立するならAbortSignal.any - 失敗した分だけの再試行は
allSettledのrejectedを再実行。同時実行数は自作pLimitで制限し、Array.fromAsyncは直列 withResolversは外から resolve できる Promise、Promise.tryは同期の throw も rejected にそろえる(Node 23 以降)- 未処理の拒否はプロセスを止める。先に作った Promise は順に
awaitせずallでまとめて待ち、残りを止めたいならAbortSignalを渡す
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Promise.all・allSettled・race・anyの違い|実行結果で比較」
株式会社RJCでは、複数の非同期処理をどう組み合わせるかといった設計の判断を、実際に動かして結果を確かめながらプロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。