何をしたいユーティリティか:「非同期 filter」
「非同期 filter」は、配列の各要素に対して「async な条件チェック」を行い、条件を満たした要素だけを残すユーティリティです。
普通の Array.prototype.filter は同期処理専用なので、次のような場面ではそのまま使えません。
API を叩いて「有効なユーザーかどうか」を判定して、OK なユーザーだけ残したい。
DB に問い合わせて「権限があるかどうか」を確認して、権限ありのレコードだけ残したい。
こういう「条件判定そのものが非同期」のときに必要になるのが、非同期版 filter(async filter)です。
前提確認:同期 filter の基本イメージ
まずは、普通の filter が何をしているかをもう一度整理します。
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter((n) => n % 2 === 0);
// [2, 4]
JavaScriptfilter は、「コールバックが true を返した要素だけ残す」関数です。
ここでは n % 2 === 0 が「偶数なら true、奇数なら false」を返す条件関数になっています。
重要なのは、この条件関数は同期であることが前提だという点です。async (n) => { ... } のような非同期関数を渡しても、filter はそれを待ってくれません。
基本形:全部並列で判定する asyncFilter(Promise.all を使う)
実装と考え方
まずは、「全部並列で条件判定をして、true になったものだけ残す」一番シンプルな形です。
async function asyncFilter(array, asyncPredicate) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncPredicate !== "function") {
return array.slice();
}
const promises = array.map((item, index) => asyncPredicate(item, index));
const results = await Promise.all(promises);
const filtered = [];
for (let i = 0; i < array.length; i++) {
if (results[i]) {
filtered.push(array[i]);
}
}
return filtered;
}
JavaScriptここでの重要ポイントをかみ砕きます。
asyncPredicate は「非同期な条件関数」(async 関数 or Promise を返す関数)。
まず普通の map で「条件判定の Promise の配列」を作る。Promise.all で「全部の判定が終わるのを待って、結果(true/false の配列)を受け取る」。
その結果配列を見ながら、元の配列から「true の位置の要素だけ」を取り出して新しい配列を作る。
つまり、「同期 filter の非同期版」を素直に書いた形です。
例題:API で「有効ユーザーかどうか」を判定して、OK なユーザーだけ残す
async function isValidUser(userId) {
// 実際には fetch などで API を叩く想定
// ここでは簡易的に「偶数 ID だけ有効」とする
await new Promise((r) => setTimeout(r, 200));
return userId % 2 === 0;
}
async function main() {
const ids = [1, 2, 3, 4, 5];
const validIds = await asyncFilter(ids, (id) => isValidUser(id));
console.log(validIds);
// [2, 4]
}
main();
JavaScript「ID ごとに API を叩いて有効かどうか判定し、有効な ID だけ残す」という処理が、asyncFilter でかなり素直に書けています。
順番に判定する asyncFilterSeries(直列版)
なぜ直列版が必要になるのか
全部並列で判定すると速いですが、業務ではこういう制約もよくあります。
API のレート制限が厳しいので、一気に叩きたくない。
DB やファイルへのアクセスを順番にやりたい。
その場合は、「1 件ずつ await してから次へ進む」直列版の async filter が欲しくなります。
実装
async function asyncFilterSeries(array, asyncPredicate) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncPredicate !== "function") {
return array.slice();
}
const filtered = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
const ok = await asyncPredicate(item, i);
if (ok) {
filtered.push(item);
}
}
return filtered;
}
JavaScriptここでは for ループを使って、1 要素ごとに await してから次へ進むようにしています。
これで、「必ず順番に判定される非同期 filter」が手に入ります。
例題:ログを順番に外部サービスに問い合わせて、「重要かどうか」を判定する
async function isImportantLog(log) {
// 実際には外部サービスに問い合わせる想定
console.log("check:", log.message);
await new Promise((r) => setTimeout(r, 200));
return log.level === "error" || log.level === "warning";
}
async function main() {
const logs = [
{ level: "info", message: "OK" },
{ level: "warning", message: "Slow" },
{ level: "error", message: "Failed" },
];
const important = await asyncFilterSeries(logs, (log) => isImportantLog(log));
console.log(important);
/*
[
{ level: "warning", message: "Slow" },
{ level: "error", message: "Failed" },
]
*/
}
main();
JavaScriptasyncFilterSeries を使うことで、「ログを 1 件ずつ順番にチェックする」という流れが保証されます。
同時実行数を制限する asyncFilterLimit(負荷とレート制限に強い形)
「全部並列は怖い、でも完全直列は遅い」をどう解決するか
API を 1000 件分叩いて判定するようなケースで、全部並列は危険です。
一方で、完全に 1 件ずつ直列でやると時間がかかりすぎることもあります。
そこで、「同時に動かす判定の数を制限する」非同期 filter があると便利です。
実装イメージ
async function asyncFilterLimit(array, asyncPredicate, limit = 5) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncPredicate !== "function") {
return array.slice();
}
if (typeof limit !== "number" || limit <= 0) {
limit = 1;
}
const results = new Array(array.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < array.length) {
const index = currentIndex;
currentIndex += 1;
const item = array[index];
const ok = await asyncPredicate(item, index);
results[index] = ok;
}
}
const workers = [];
const workerCount = Math.min(limit, array.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
const filtered = [];
for (let i = 0; i < array.length; i++) {
if (results[i]) {
filtered.push(array[i]);
}
}
return filtered;
}
JavaScript重要ポイントをかみ砕きます。
limit は「同時に動かす最大数」。worker という「仕事を取りに行く非同期関数」を複数立ち上げる。currentIndex を共有して、「まだ判定していないインデックス」を取り合う。
各 worker は「仕事がなくなるまでループ」して判定する。
最後に results を見て、true の位置の要素だけを集める。
これで、「最大 limit 個まで並列で動く非同期 filter」が実現できます。
例題:ユーザー ID を最大 3 並列で判定する
async function isValidUser(id) {
console.log("check start", id);
await new Promise((r) => setTimeout(r, 300));
console.log("check end", id);
return id % 2 === 0;
}
async function main() {
const ids = [1, 2, 3, 4, 5, 6];
const validIds = await asyncFilterLimit(ids, (id) => isValidUser(id), 3);
console.log(validIds);
// [2, 4, 6]
}
main();
JavaScriptログを見てみると、「常に最大 3 件まで同時に判定している」ことが分かるはずです。
これが、レート制限や負荷を意識した“業務レベルの非同期 filter”です。
非同期 filter を使うときの重要な意識ポイント
戻り値は必ず Promise になる
asyncFilter / asyncFilterSeries / asyncFilterLimit は、どれも async function です。
つまり、戻り値は必ず Promise になります。
使う側は必ず await するか、.then(...) で受け取る必要があります。
const p = asyncFilter(ids, isValidUser); // これは Promise
const validIds = await asyncFilter(ids, isValidUser); // これで配列になる
JavaScriptここを忘れると、「配列だと思っていたら Promise だった」という典型的なハマり方をします。
条件関数の戻り値は「真偽値(またはそれに変換できるもの)」にする
asyncPredicate は、「その要素を残すかどうか」を決める関数です。
戻り値は基本的に true/false にしておくと、読みやすくなります。
数値や文字列を返しても JavaScript 的には真偽値に変換されますが、
業務コードでは「この関数は true/false を返す」と決めておいたほうが、意図が伝わりやすいです。
エラーをどう扱うかを決める
非同期判定の中でエラーが起きたときに、
その要素を「落とす(false とみなす)」のか。
全体を「失敗(reject)」とみなすのか。
を、ユーティリティのルールとして決めておくとよいです。
例えば、「エラーならその要素は無効として落とす」なら、こう書けます。
async function safePredicate(item) {
try {
return await asyncPredicate(item);
} catch (e) {
return false;
}
}
JavaScriptこのように、「エラーをどう扱うか」も含めて非同期 filter の設計になります。
手を動かして非同期 filter の感覚をつかむ
次のコードをコンソール(Node など)で実行して、挙動を自分の目で確認してみてください。
async function asyncFilter(array, asyncPredicate) {
if (!Array.isArray(array)) return [];
if (typeof asyncPredicate !== "function") return array.slice();
const promises = array.map((item, index) => asyncPredicate(item, index));
const results = await Promise.all(promises);
const filtered = [];
for (let i = 0; i < array.length; i++) {
if (results[i]) {
filtered.push(array[i]);
}
}
return filtered;
}
async function asyncFilterSeries(array, asyncPredicate) {
if (!Array.isArray(array)) return [];
if (typeof asyncPredicate !== "function") return array.slice();
const filtered = [];
for (let i = 0; i < array.length; i++) {
const ok = await asyncPredicate(array[i], i);
if (ok) {
filtered.push(array[i]);
}
}
return filtered;
}
async function asyncFilterLimit(array, asyncPredicate, limit = 2) {
if (!Array.isArray(array)) return [];
if (typeof asyncPredicate !== "function") return array.slice();
if (typeof limit !== "number" || limit <= 0) limit = 1;
const results = new Array(array.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < array.length) {
const index = currentIndex;
currentIndex += 1;
const ok = await asyncPredicate(array[index], index);
results[index] = ok;
}
}
const workers = [];
const workerCount = Math.min(limit, array.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
const filtered = [];
for (let i = 0; i < array.length; i++) {
if (results[i]) {
filtered.push(array[i]);
}
}
return filtered;
}
async function demo() {
const ids = [1, 2, 3, 4];
console.log("=== asyncFilter (parallel) ===");
console.log(
await asyncFilter(ids, async (id) => {
console.log("check", id);
await new Promise((r) => setTimeout(r, 300));
return id % 2 === 0;
})
);
console.log("=== asyncFilterSeries (series) ===");
console.log(
await asyncFilterSeries(ids, async (id) => {
console.log("check", id);
await new Promise((r) => setTimeout(r, 300));
return id % 2 === 0;
})
);
console.log("=== asyncFilterLimit (limit=2) ===");
console.log(
await asyncFilterLimit(ids, async (id) => {
console.log("check", id);
await new Promise((r) => setTimeout(r, 300));
return id % 2 === 0;
}, 2)
);
}
demo();
JavaScript並列・直列・同時実行数制限の違いが、ログの出方で体感できるはずです。
まとめ:非同期 filter ユーティリティで「配列 × async 条件」を標準化する
業務コードでは、「配列の各要素に対して非同期な条件判定をして、OK なものだけ残す」場面が本当に多いです。
そのたびに Promise.all や for ループをベタ書きするのではなく、
export async function asyncFilter(...) { ... } // 全並列
export async function asyncFilterSeries(...) { ... } // 直列
export async function asyncFilterLimit(...) { ... } // 同時実行数制限
JavaScriptのようなユーティリティを用意して、「配列 × async 条件は必ずこれを通す」と決めておくと、
コードの意図が一気に読みやすくなります。
「速さを優先するのか」「順番を守るのか」「負荷を抑えるのか」――
その選択を関数名に埋め込んでおくことが、非同期処理を“業務レベル”で扱ううえでの大事な設計になります。
