何をしたいユーティリティか:「非同期 map」
「非同期 map」は、配列の各要素に対して「async な処理」をして、その結果を配列として集めるユーティリティです。
普通の map は同期処理専用ですが、業務ではこういうケースがよく出てきます。
API を配列分だけ叩いて、そのレスポンスを配列で受け取りたい。
ファイルや DB を順番に読み書きして、その結果を配列にしたい。
ここで大事なのは、「全部並列でやっていいのか」「順番にやりたいのか」「同時実行数を制限したいのか」を、ユーティリティとして決めておくことです。
基本形:全部並列で実行する asyncMap(Promise.all)
実装と考え方
まずは「全部並列で実行して、全部終わったら結果を返す」一番シンプルな形です。
async function asyncMap(array, asyncMapper) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncMapper !== "function") {
return array.slice();
}
const promises = array.map((item, index) => asyncMapper(item, index));
return Promise.all(promises);
}
JavaScript重要ポイントをかみ砕きます。
配列じゃなければ空配列を返す。asyncMapper は「非同期な変換関数」(async 関数 or Promise を返す関数)。
まず普通の map で「Promise の配列」を作る。Promise.all で「全部の Promise が終わるのを待って、結果を配列で受け取る」。
つまり、「同期 map の非同期版」を素直に書いた形です。
例題:ユーザー ID の配列から、API でユーザー情報を取得する
async function fetchUser(id) {
// 実際には fetch などで API を叩く想定
return { id, name: `User-${id}` };
}
async function main() {
const ids = [1, 2, 3];
const users = await asyncMap(ids, (id) => fetchUser(id));
console.log(users);
// [
// { id: 1, name: "User-1" },
// { id: 2, name: "User-2" },
// { id: 3, name: "User-3" },
// ]
}
main();
JavaScriptasyncMap のおかげで、「ID 配列 → ユーザー情報配列」という流れが、同期 map とほぼ同じ感覚で書けます。
順番に実行する asyncMapSeries(直列版)
なぜ直列版が必要になるのか
全部並列で実行すると速いですが、業務ではこういう制約もあります。
API のレート制限が厳しいので、一気に叩きたくない。
DB やファイルへのアクセスを順番にやりたい。
その場合は、「1 件ずつ await してから次へ進む」直列版が欲しくなります。
実装
async function asyncMapSeries(array, asyncMapper) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncMapper !== "function") {
return array.slice();
}
const result = [];
for (let i = 0; i < array.length; i++) {
const value = await asyncMapper(array[i], i);
result.push(value);
}
return result;
}
JavaScriptここでは for ループを使って、1 要素ごとに await してから次へ進むようにしています。
これで、「必ず順番に実行される非同期 map」が手に入ります。
例題:ログを順番に書き込む
async function writeLog(line) {
// 実際にはファイルや外部サービスに書き込む想定
console.log("write:", line);
}
async function main() {
const lines = ["A", "B", "C"];
await asyncMapSeries(lines, (line) => writeLog(line));
console.log("done");
}
main();
JavaScriptasyncMapSeries を使うことで、「A → B → C の順に書き込む」という順序が保証されます。
同時実行数を制限する asyncMapLimit(業務でかなり実用的)
「全部並列は怖い、でも完全直列は遅い」をどう解決するか
API を 1000 件分叩くときに、全部並列で投げるのは危険です。
一方で、完全に 1 件ずつ直列でやると時間がかかりすぎることもあります。
そこで、「同時に動かす数を制限する」ユーティリティがあると便利です。
実装イメージ
async function asyncMapLimit(array, asyncMapper, limit = 5) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncMapper !== "function") {
return array.slice();
}
if (typeof limit !== "number" || limit <= 0) {
limit = 1;
}
const result = new Array(array.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < array.length) {
const index = currentIndex;
currentIndex += 1;
const value = await asyncMapper(array[index], index);
result[index] = value;
}
}
const workers = [];
const workerCount = Math.min(limit, array.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return result;
}
JavaScript重要ポイントをかみ砕きます。
limit は「同時に動かす最大数」。worker という「仕事を取りに行く非同期関数」を複数立ち上げる。currentIndex を共有して、「まだ処理していないインデックス」を取り合う。
各 worker は「仕事がなくなるまでループ」して処理する。
最後に Promise.all(workers) で、全部の worker が終わるのを待つ。
これで、「最大 limit 個まで並列で動く非同期 map」が実現できます。
例題:API を最大 3 並列で叩く
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 users = await asyncMapLimit(ids, (id) => fetchUser(id), 3);
console.log(users);
}
main();
JavaScriptログを見てみると、「常に最大 3 件まで同時に動いている」ことが分かるはずです。
これが、レート制限や負荷を意識した“業務レベルの非同期 map”です。
非同期 map を使うときの重要な意識ポイント
「戻り値は Promise(async 関数)になる」
asyncMap / asyncMapSeries / asyncMapLimit は、どれも async function です。
つまり、戻り値は必ず Promise になります。
使う側は必ず await するか、.then(...) で受け取る必要があります。
const result = asyncMap(ids, fetchUser); // これは Promise
const users = await asyncMap(ids, fetchUser); // これで配列になる
JavaScriptここを忘れると、「配列だと思っていたら Promise だった」という典型的なハマり方をします。
「エラーはどう扱うか」を決める
Promise.all は、1 つでも reject すると全体が reject します。
業務によっては、「失敗したものだけスキップしたい」「成功したものだけ集めたい」などの要件も出てきます。
その場合は、asyncMapper の中で try/catch して「失敗時は null を返す」など、
「エラーをどう扱うか」をユーティリティのルールとして決めておくとよいです。
手を動かして非同期 map の感覚をつかむ
次のコードをコンソール(Node など)で実行して、挙動を自分の目で確認してみてください。
async function asyncMap(array, asyncMapper) {
if (!Array.isArray(array)) return [];
if (typeof asyncMapper !== "function") return array.slice();
const promises = array.map((item, index) => asyncMapper(item, index));
return Promise.all(promises);
}
async function asyncMapSeries(array, asyncMapper) {
if (!Array.isArray(array)) return [];
if (typeof asyncMapper !== "function") return array.slice();
const result = [];
for (let i = 0; i < array.length; i++) {
const value = await asyncMapper(array[i], i);
result.push(value);
}
return result;
}
async function asyncMapLimit(array, asyncMapper, limit = 2) {
if (!Array.isArray(array)) return [];
if (typeof asyncMapper !== "function") return array.slice();
if (typeof limit !== "number" || limit <= 0) limit = 1;
const result = new Array(array.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < array.length) {
const index = currentIndex;
currentIndex += 1;
const value = await asyncMapper(array[index], index);
result[index] = value;
}
}
const workers = [];
const workerCount = Math.min(limit, array.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return result;
}
async function demo() {
const ids = [1, 2, 3, 4];
console.log("=== asyncMap (parallel) ===");
await asyncMap(ids, async (id) => {
console.log("start", id);
await new Promise((r) => setTimeout(r, 300));
console.log("end", id);
return id * 10;
});
console.log("=== asyncMapSeries (series) ===");
await asyncMapSeries(ids, async (id) => {
console.log("start", id);
await new Promise((r) => setTimeout(r, 300));
console.log("end", id);
return id * 10;
});
console.log("=== asyncMapLimit (limit=2) ===");
await asyncMapLimit(ids, async (id) => {
console.log("start", id);
await new Promise((r) => setTimeout(r, 300));
console.log("end", id);
return id * 10;
}, 2);
}
demo();
JavaScript並列・直列・同時実行数制限の違いが、ログの出方で体感できるはずです。
まとめ:非同期 map ユーティリティで「配列 × async」を標準化する
業務コードでは、「配列の各要素に対して非同期処理をする」場面が本当に多いです。
そのたびに Promise.all や for ループをベタ書きするのではなく、
export async function asyncMap(...) { ... } // 全並列
export async function asyncMapSeries(...) { ... } // 直列
export async function asyncMapLimit(...) { ... } // 同時実行数制限
JavaScriptのようなユーティリティを用意して、「配列 × async は必ずこれを通す」と決めておくと、
コードの意図が一気に読みやすくなります。
「速さを優先するのか」「順番を守るのか」「負荷を抑えるのか」――
その選択を関数名に埋め込んでおくことが、非同期処理を“業務レベル”で扱ううえでの大事な設計になります。
