テーマの整理:「Promise 配列制御」とは何か
「Promise 配列制御」というのは、ざっくり言うと
「複数の非同期処理(Promise)を、配列としてまとめて扱い、どう待つか・どう制御するかを決めるテクニック」です。
業務だと、こんな状況がよく出てきます。
複数ユーザーの情報を API でまとめて取得したい。
複数ファイルを並列でアップロードしたい。
複数のチェック処理を投げて、全部終わってから結果を集計したい。
こういうとき、Promise をバラバラに扱うのではなく、
「Promise の配列」としてまとめて制御できると、コードが一気に整理されます。
ここでは、実務でよく使うユーティリティ的な考え方を、初心者向けにかみ砕いて説明していきます。
Promise.all 系の基本ユーティリティ
Promise.all:全部成功したら結果を配列で返す
Promise.all は、「全部の Promise が成功したら、結果を配列で返す」関数です。
1 つでも失敗(reject)すると、全体が reject になります。
function all(promises) {
return Promise.all(promises);
}
JavaScriptこれはラッパーというより「名前を短くしただけ」ですが、
「Promise 配列をまとめて待つ」という意味をはっきりさせるために、
自分のユーティリティとして all という名前で使うのもアリです。
例題として、複数ユーザーを並列で取得するコードを見てみます。
async function fetchUser(id) {
// 実際には fetch などで API を叩く想定
await new Promise((r) => setTimeout(r, 200));
return { id, name: `User-${id}` };
}
async function main() {
const ids = [1, 2, 3];
const promises = ids.map((id) => fetchUser(id));
const users = await all(promises);
console.log(users);
// [{ id:1, ... }, { id:2, ... }, { id:3, ... }]
}
main();
JavaScriptここで重要なのは、
「Promise の配列を作る」→「all で一気に待つ」というパターンを体で覚えることです。
Promise.allSettled:成功・失敗を全部知りたいとき
Promise.allSettled は、「全部の Promise が終わるまで待ち、成功か失敗かを含めて結果を返す」関数です。
途中で失敗しても、全体は reject せず、最後まで待ちます。
ユーティリティとしては、例えばこうラップしておくと分かりやすくなります。
function allSettled(promises) {
return Promise.allSettled(promises);
}
JavaScript例題として、「成功したものだけを取り出す」処理を書いてみます。
async function maybeFail(id) {
await new Promise((r) => setTimeout(r, 200));
if (id % 2 === 0) {
throw new Error("failed: " + id);
}
return { id, ok: true };
}
async function main() {
const ids = [1, 2, 3, 4];
const promises = ids.map((id) => maybeFail(id));
const results = await allSettled(promises);
const fulfilled = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
console.log(fulfilled);
// [{ id:1, ok:true }, { id:3, ok:true }]
}
main();
JavaScript重要ポイントは、
「失敗しても全体が落ちない」「成功・失敗を後から自分で仕分けできる」ことです。
業務では、「一部失敗しても処理を続けたい」ケースが多いので、allSettled 系のユーティリティはかなり実用的です。
race / any 系のユーティリティ
Promise.race:一番早く終わったものだけ欲しい
Promise.race は、「最初に settle(成功 or 失敗)した Promise の結果だけを返す」関数です。
タイムアウト処理などでよく使われます。
ユーティリティとしては、例えばこうです。
function race(promises) {
return Promise.race(promises);
}
JavaScript例題として、「API とタイムアウトのどちらが先か」を見るコードを書いてみます。
async function fetchSlow() {
await new Promise((r) => setTimeout(r, 1000));
return "OK";
}
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("timeout")), ms);
});
}
async function main() {
try {
const result = await race([fetchSlow(), timeout(500)]);
console.log(result);
} catch (e) {
console.log("error:", e.message); // "timeout"
}
}
main();
JavaScript「Promise の配列を race に渡すと、一番早く終わったものだけが採用される」という感覚を持っておくと、
タイムアウトやフォールバック処理をきれいに書けるようになります。
Promise.any:どれか 1 つでも成功すれば OK
Promise.any は、「どれか 1 つでも成功したら、その結果を返す」関数です。
全部失敗したときだけ reject します。
ユーティリティとしては、こうラップできます。
function any(promises) {
return Promise.any(promises);
}
JavaScript例題として、「複数のエンドポイントのうち、どれか 1 つでも応答してくれればいい」というケースを考えます。
async function fetchFromA() {
await new Promise((r) => setTimeout(r, 800));
return "A";
}
async function fetchFromB() {
await new Promise((r) => setTimeout(r, 300));
return "B";
}
async function main() {
const result = await any([fetchFromA(), fetchFromB()]);
console.log(result); // "B"
}
main();
JavaScript「どれか 1 つ成功すればいい」という要件は意外と多いので、any という名前でユーティリティ化しておくと、意図が読みやすくなります。
同時実行数を制御するユーティリティ(Promise 配列制御の“本命”)
なぜ「同時実行数」が重要なのか
Promise を配列で扱うとき、
「全部一気に実行する(完全並列)」か「1 個ずつ順番に実行する(完全直列)」か、
このどちらかだけだと、現実の業務では困ることが多いです。
API のレート制限があるので、一度に大量に叩きたくない。
でも、完全に 1 個ずつだと遅すぎる。
そこで、「同時に動かす Promise の数を制限する」ユーティリティがとても役に立ちます。
実装例:runWithLimit(最大 N 個まで並列実行)
async function runWithLimit(tasks, limit = 5) {
if (!Array.isArray(tasks)) {
return [];
}
if (typeof limit !== "number" || limit <= 0) {
limit = 1;
}
const results = new Array(tasks.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < tasks.length) {
const index = currentIndex;
currentIndex += 1;
const task = tasks[index];
try {
results[index] = await task();
} catch (e) {
results[index] = e;
}
}
}
const workers = [];
const workerCount = Math.min(limit, tasks.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return results;
}
JavaScriptここでの重要ポイントを丁寧に分解します。
tasks は「関数の配列」です。
各関数は呼び出されると Promise を返す、つまり「まだ実行していない非同期処理」です。currentIndex で「次に実行すべきタスクの位置」を共有します。worker は「タスクを取りに行って実行する役割」を持つ非同期関数です。
worker を limit 個だけ立ち上げることで、「同時に動くタスクの数」を制限します。
全部の worker が終わったら、results に全タスクの結果が入っています。
これが、Promise 配列制御の中でもかなり実務的なパターンです。
例題:最大 3 並列で API を叩く
async function fetchUser(id) {
console.log("start", id);
await new Promise((r) => setTimeout(r, 500));
console.log("end", id);
return { id, name: `User-${id}` };
}
async function main() {
const ids = [1, 2, 3, 4, 5, 6];
const tasks = ids.map((id) => () => fetchUser(id));
const results = await runWithLimit(tasks, 3);
console.log(results);
}
main();
JavaScriptログを眺めると、「常に最大 3 つまでしか同時に動いていない」ことが分かるはずです。
これがまさに「Promise 配列を、同時実行数という観点で制御している」状態です。
Promise 配列制御で意識してほしい“設計の軸”
軸 1:いつ全部待つか、いつ一部だけでいいか
全部終わるまで待ちたいなら all や allSettled。
一番早いものだけ欲しいなら race。
どれか 1 つ成功すればいいなら any。
「この処理は、どのタイミングで“完了”とみなしたいのか?」を、
関数名レベルで表現しておくと、コードを読む人が迷いません。
軸 2:同時実行数をどうするか
完全並列でいいのか。
完全直列にしたいのか。
最大 N 個まで並列にしたいのか。
これも、runWithLimit のようなユーティリティ名に意図を埋め込んでおくと、
「この処理は負荷を意識しているんだな」と一目で分かります。
軸 3:失敗をどう扱うか
1 つでも失敗したら全体を失敗にするのか(all 的)。
失敗しても最後まで全部見てから判断したいのか(allSettled 的)。
失敗したものは結果配列にエラーとして入れておくのか。
業務では、「一部失敗は許容するが、ログには残したい」などの要件が多いので、
「エラーをどう扱うか」も Promise 配列制御の一部として設計しておくと良いです。
手を動かして「Promise 配列制御」の感覚をつかむ
まとめて試せる小さなサンプルを書きます。
Node などで実行して、ログの出方を眺めてみてください。
function all(promises) {
return Promise.all(promises);
}
function allSettled(promises) {
return Promise.allSettled(promises);
}
function race(promises) {
return Promise.race(promises);
}
function any(promises) {
return Promise.any(promises);
}
async function runWithLimit(tasks, limit = 2) {
if (!Array.isArray(tasks)) return [];
if (typeof limit !== "number" || limit <= 0) limit = 1;
const results = new Array(tasks.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < tasks.length) {
const index = currentIndex;
currentIndex += 1;
const task = tasks[index];
try {
results[index] = await task();
} catch (e) {
results[index] = e;
}
}
}
const workers = [];
const workerCount = Math.min(limit, tasks.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return results;
}
async function demo() {
const ids = [1, 2, 3, 4];
console.log("=== all ===");
console.log(
await all(
ids.map(async (id) => {
await new Promise((r) => setTimeout(r, 200 * id));
return id * 10;
})
)
);
console.log("=== allSettled ===");
console.log(
await allSettled(
ids.map(async (id) => {
await new Promise((r) => setTimeout(r, 200 * id));
if (id % 2 === 0) throw new Error("fail " + id);
return id * 10;
})
)
);
console.log("=== race ===");
console.log(
await race(
ids.map(async (id) => {
await new Promise((r) => setTimeout(r, 200 * id));
return id;
})
)
);
console.log("=== any ===");
console.log(
await any(
ids.map(async (id) => {
await new Promise((r) => setTimeout(r, 200 * id));
if (id < 3) throw new Error("fail " + id);
return id;
})
)
);
console.log("=== runWithLimit (limit=2) ===");
const tasks = ids.map((id) => async () => {
console.log("start", id);
await new Promise((r) => setTimeout(r, 500));
console.log("end", id);
return id * 100;
});
console.log(await runWithLimit(tasks, 2));
}
demo();
JavaScript「どの関数がどんなふうに Promise 配列を制御しているか」を、
ログと結果を見ながら体で覚えていくと、実務での使いどころが見えてきます。
まとめ:Promise 配列制御は「非同期を怖くなくするための設計」
配列ユーティリティとしての「Promise 配列制御」は、
単に便利な関数というより、「非同期処理をどう設計するか」という考え方そのものです。
全部待つのか、一部でいいのか。
全部並列なのか、順番なのか、制限付き並列なのか。
失敗をどう扱うのか。
これらをユーティリティ関数の名前と振る舞いに落とし込んでおくと、
「このコードは何を意図しているのか」が、読み手にちゃんと伝わるようになります。
そこまでいくと、非同期処理はもう“怖いもの”ではなく、
「ちゃんとコントロールできる道具」になっていきます。
