テーマの整理:「順次処理」とは何か
ここでいう「順次処理」は、配列の要素を「必ず 1 件ずつ、順番に」処理していくパターンのことです。
特に「処理が非同期(async)」なときに、順番をきちんと守りたい場面で使います。
同期処理なら for や forEach で自然と順番になりますが、
非同期処理を map や Promise.all で一気に投げると、「順番」も「同時実行数」も制御しづらくなります。
そこで、「配列を順番に処理するためのユーティリティ」を用意しておくと、業務コードがかなり安定します。
なぜ「順次処理」ユーティリティが必要になるのか
順次処理が欲しくなる典型的な理由は、だいたい次のようなものです。
API のレート制限が厳しく、一気に大量に叩きたくない。
ログやファイルを書き込む順番を保証したい。
前の処理結果を見てから、次の処理内容を決めたい。
こういうときに Promise.all で全部並列にしてしまうと、
「速いけど危ない・制御しづらい」状態になります。
逆に、「1 件ずつ順番にやる」と決めてしまえば、挙動がとても分かりやすくなります。
基本ユーティリティ 1:forEachSeries(順番に処理するだけ)
実装と考え方
まずは、「配列の各要素に対して、非同期処理を順番に実行するだけ」の一番シンプルなユーティリティです。
async function forEachSeries(array, asyncFn) {
if (!Array.isArray(array)) {
return;
}
if (typeof asyncFn !== "function") {
return;
}
for (let i = 0; i < array.length; i++) {
await asyncFn(array[i], i);
}
}
JavaScriptここでの重要ポイントをかみ砕きます。
asyncFn は「要素を 1 つ受け取って処理する async 関数」です。for ループの中で、毎回 await asyncFn(...) してから次の要素に進みます。
つまり、「前の処理が終わるまで、次の処理は絶対に始まらない」ことが保証されます。
戻り値は特に使わず、「順番に処理を流したいだけ」のときに向いています。
例題:ログを順番に書き出す
async function writeLog(line) {
await new Promise((r) => setTimeout(r, 200));
console.log("write:", line);
}
async function main() {
const lines = ["A", "B", "C"];
await forEachSeries(lines, (line) => writeLog(line));
console.log("done");
}
main();
JavaScriptログの出方を見ると、「A → B → C」の順番が必ず守られているはずです。
ここで Promise.all を使ってしまうと、順番は保証されません。
基本ユーティリティ 2:mapSeries(順番に処理して結果を集める)
実装と考え方
次に、「順番に処理しつつ、その結果を配列として集めたい」パターンです。
これは、以前出てきた asyncMapSeries と同じ発想です。
async function mapSeries(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重要ポイントはこうです。
asyncMapper は「要素を変換して結果を返す async 関数」です。
毎回 await してから結果を result に push します。
処理順も、結果の順番も、元の配列と同じになります。
例題:ユーザー ID を順番に API で取得する
async function fetchUser(id) {
console.log("start", id);
await new Promise((r) => setTimeout(r, 300));
console.log("end", id);
return { id, name: `User-${id}` };
}
async function main() {
const ids = [1, 2, 3];
const users = await mapSeries(ids, (id) => fetchUser(id));
console.log(users);
}
main();
JavaScriptログを見ると、「1 の開始→終了 → 2 の開始→終了 → 3 の開始→終了」という順番で動いているのが分かります。
「順番を守りたい API 呼び出し」には、この形がとても相性が良いです。
応用ユーティリティ:順次処理+途中で止める(早期終了)
シナリオ
「配列を順番に処理していき、ある条件を満たしたらそこで止めたい」というケースもよくあります。
例えば、
最初に成功した結果だけ欲しい。
最初にエラーになったところで止めたい。
といったパターンです。
実装例:findSeries(順番に処理して、最初に条件を満たした結果を返す)
async function findSeries(array, asyncPredicate) {
if (!Array.isArray(array)) {
return undefined;
}
if (typeof asyncPredicate !== "function") {
return undefined;
}
for (let i = 0; i < array.length; i++) {
const item = array[i];
const ok = await asyncPredicate(item, i);
if (ok) {
return item;
}
}
return undefined;
}
JavaScriptこれは「非同期版の find」です。
順番に判定していき、最初に true になった要素を返します。
例題:最初に「有効」と判定されたサーバーだけ使う
async function isHealthyServer(url) {
console.log("check:", url);
await new Promise((r) => setTimeout(r, 300));
return url.includes("primary");
}
async function main() {
const servers = [
"https://backup-1.example.com",
"https://primary.example.com",
"https://backup-2.example.com",
];
const server = await findSeries(servers, (url) => isHealthyServer(url));
console.log("use:", server);
}
main();
JavaScriptここでは、「順番にサーバーをチェックして、最初に OK だったものだけ使う」という流れを、findSeries で素直に表現できています。
順次処理ユーティリティを使うときの重要な意識ポイント
「順番が意味を持つか?」を必ず自分に問う
順次処理を選ぶか、並列処理を選ぶかは、性能にも影響する大事な判断です。
そこで、毎回自分にこう問いかけてほしいです。
この処理は、前の結果に依存しているか。
外部サービスやレート制限の都合で、同時実行数を抑えたいか。
ログやファイルの「順番」が意味を持つか。
どれか 1 つでも「はい」なら、順次処理ユーティリティを使う価値が高いです。
「for + await」を“パターン”として覚える
順次処理の本質は、実はとてもシンプルです。
for (...) {
await ...
}
JavaScriptこれを毎回生で書くのではなく、forEachSeries や mapSeries のような名前付きユーティリティにしておくと、
「これは順次処理なんだな」と一目で分かるようになります。
戻り値が Promise になることを忘れない
forEachSeries も mapSeries も findSeries も、async function なので戻り値は Promise です。
呼び出し側は必ず await する必要があります。
await forEachSeries(...);
const result = await mapSeries(...);
const found = await findSeries(...);
JavaScriptここを忘れて「配列だと思っていたら Promise だった」というのは、非同期あるあるなので、
「順次処理ユーティリティ=必ず await」とセットで覚えておくと安全です。
手を動かして「順次処理」の感覚をつかむ
次のコードをそのまま実行して、ログの出方を眺めてみてください。
async function forEachSeries(array, asyncFn) {
if (!Array.isArray(array)) return;
if (typeof asyncFn !== "function") return;
for (let i = 0; i < array.length; i++) {
await asyncFn(array[i], i);
}
}
async function mapSeries(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 findSeries(array, asyncPredicate) {
if (!Array.isArray(array)) return undefined;
if (typeof asyncPredicate !== "function") return undefined;
for (let i = 0; i < array.length; i++) {
const item = array[i];
const ok = await asyncPredicate(item, i);
if (ok) return item;
}
return undefined;
}
async function demo() {
const items = [1, 2, 3];
console.log("=== forEachSeries ===");
await forEachSeries(items, async (n) => {
console.log("start", n);
await new Promise((r) => setTimeout(r, 300));
console.log("end", n);
});
console.log("=== mapSeries ===");
const doubled = await mapSeries(items, async (n) => {
await new Promise((r) => setTimeout(r, 200));
return n * 2;
});
console.log(doubled);
console.log("=== findSeries ===");
const found = await findSeries(items, async (n) => {
await new Promise((r) => setTimeout(r, 200));
return n >= 2;
});
console.log(found);
}
demo();
JavaScript「必ず 1 → 2 → 3 の順に動いていること」「結果の順番も元の配列と同じであること」を、
ログと出力で確認してみてください。
まとめ:順次処理ユーティリティで「安全な非同期フロー」を標準化する
配列ユーティリティとしての「順次処理」は、
非同期処理を「速さ」ではなく「安全さ・分かりやすさ」優先で設計するための道具です。
プロジェクトに例えば次のような関数を置いておくイメージです。
export async function forEachSeries(...) { ... }
export async function mapSeries(...) { ... }
export async function findSeries(...) { ... }
JavaScriptそして、「順番が意味を持つ処理は必ずこれを通す」と決めておく。
それだけで、非同期コードのバグが減り、挙動が読みやすくなり、
「どこで並列にしてよくて、どこは順次でなきゃいけないか」がチーム全体で共有しやすくなります。
