Contents
1️⃣ AsyncIterator プロトコルの基本
1.1 定義と MDN の記述
- AsyncIterator は
next()が常にPromise<{ value, done }>を返すイテレータです。 - 同期イテレータが
Symbol.iterator、非同期イテレータはSymbol.asyncIteratorで判定されます。
MDN(2024 年時点): 「AsyncIterator オブジェクトは非同期イテレータープロトコルに準拠し、
next()メソッドが 履行された プロミスを返す」【MDN】。
1.2 なぜ Promise が必須か
- 非同期データ(API ストリーム、ファイル読み込みなど)は取得に時間が掛かる。
awaitと組み合わせてfor‑await‑ofが自動的にawait iterator.next()を呼び出すことで、「非同期処理をイテレーションの中に自然に埋め込める」 という利点が得られます。
1.3 最小実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const asyncCounter = { i: 0, async next() { if (this.i < 3) { await new Promise(r => setTimeout(r, 100)); // 100 ms 待機 return { value: this.i++, done: false }; } return { value: undefined, done: true }; }, [Symbol.asyncIterator]() { return this; } }; (async () => { for await (const n of asyncCounter) console.log(n); // → 0 1 2 })(); |
2️⃣ async function* と for‑await‑of
2.1 非同期ジェネレータのシンタックス
|
1 2 3 4 5 6 7 8 |
async function* fetchNumbers(urls) { for (const url of urls) { const resp = await fetch(url); const { num } = await resp.json(); yield num; // 次のイテレーションへ値を送出 } } |
- 戻り値は
AsyncGenerator<T>({ next(): Promise<IteratorResult<T>> })です。 yieldとawaitが同居でき、「非同期データ取得ロジックがシンプルに」 なります。
2.2 for‑await‑of の動作フロー
|
1 2 3 4 5 6 7 8 |
const urls = ['…/1', '…/2', '…/3']; (async () => { for await (const n of fetchNumbers(urls)) { console.log('received:', n); } })(); |
for‑await‑ofは内部で次の手順を実行しますiterator = iterable[Symbol.asyncIterator]()result = await iterator.next()done ? break : body(result.value)
2.3 同期ジェネレータとの主な違い
| 項目 | 同期 (function*) |
非同期 (async function*) |
|---|---|---|
next() の戻り値 |
{value, done}(即時) |
Promise<{value, done}> |
await の有無 |
使用不可 | 任意に使用可能 |
| 例外伝搬 | 同期的にスロー | Promise が reject → try/catch で捕捉 |
3️⃣ カスタム async iterator の作り方
3.1 Symbol.asyncIterator を実装するだけ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PagingAsyncIterator { constructor(fetchPage, pageSize = 20) { this.fetchPage = fetchPage; // (token) => Promise<{ items, nextToken }> this.nextToken = null; this.done = false; } async next() { if (this.done) return { value: undefined, done: true }; const { items, nextToken } = await this.fetchPage(this.nextToken); this.nextToken = nextToken; if (!nextToken) this.done = true; return { value: items, done: false }; } [Symbol.asyncIterator]() { return this; } } |
3.2 ページング API のサンプル
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
async function mockFetchPage(token) { const page = token ? Number(token) + 1 : 1; if (page > 3) return { items: [], nextToken: null }; return { items: Array.from({ length: 5 }, (_, i) => `item-${(page - 1) * 5 + i + 1}`), nextToken: String(page) }; } (async () => { const it = new PagingAsyncIterator(mockFetchPage); for await (const batch of it) console.log('batch:', batch); })(); |
3.3 同期イテレータを非同期にラップ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function asyncWrap(iterable, delay = 0) { const iterator = iterable[Symbol.iterator](); return { async next() { const r = iterator.next(); if (r.done) return { value: undefined, done: true }; await new Promise(res => setTimeout(res, delay)); return { value: r.value, done: false }; }, [Symbol.asyncIterator]() { return this; } }; } // 使用例 const syncArray = [10, 20, 30]; (async () => { for await (const v of asyncWrap(syncArray, 50)) console.log(v); })(); |
4️⃣ エラーハンドリングとキャンセル
4.1 try / catch で例外を捕捉
|
1 2 3 4 5 6 7 8 9 10 11 12 |
async function* faultyGen() { yield 1; throw new Error('途中で失敗'); } (async () => { try { for await (const v of faultyGen()) console.log(v); } catch (e) { console.error('イテレーション中にエラー:', e.message); } })(); |
next()が reject された瞬間、for‑await‑ofは例外を投げます。
4.2 AbortSignal による安全な中断(2024‑2026 年の実装状況)
- AbortController / AbortSignal は Web 標準として既に実装済みです。
- 最近のブラウザ(Chrome 124、Edge 124、Safari 17 プレビュー)と Node.js 20 以降では
signal.throwIfAborted()が提供されています(WHATWG の abort signal 仕様の一部)。 - ただしこのメソッドは「2025 年に追加された」等の年次表記を避け、「最新実装で利用可能」 と説明します。
中断チェック例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
async function* cancellableGen(urls, signal) { for (const url of urls) { // AbortSignal の API が存在すれば利用 if (typeof signal.throwIfAborted === 'function') { signal.throwIfAborted(); // AbortError を即時スロー } else if (signal.aborted) { throw new DOMException('中断されました', 'AbortError'); } const resp = await fetch(url, { signal }); const data = await resp.json(); yield data; } } const ctrl = new AbortController(); (async () => { try { for await (const d of cancellableGen(['a','b'], ctrl.signal)) { console.log(d); if (/* 条件 */ false) ctrl.abort(); // 任意タイミングで中断 } } catch (e) { if (e.name === 'AbortError') console.warn('イテレータが中断されました'); else console.error(e); } finally { console.log('クリーンアップ完了'); } })(); |
4.3 finally ブロックでリソース解放
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
async function* fileLineReader(fileHandle, signal) { const stream = fileHandle.createReadStream({ encoding: 'utf8' }); try { for await (const chunk of stream) { if (signal.aborted) break; yield* chunk.split('\n'); } } finally { await stream.destroy(); // ストリーム破棄 await fileHandle.close(); // ファイルハンドルクローズ } } |
breakや例外で途中終了した場合でもfinallyが必ず実行され、リーク防止 が保証されます。
5️⃣ Async Iterator Helpers(提案段階)
重要:2024 年時点では async iterator helpers(
.map(),.filter(),.take(),.flatMap()など)は TC39 の Stage 3 提案 です。Chrome 124、Edge 124、Safari 17 プレビュー、Node.js 20 で実装が始まっていますが、標準化はまだ確定していません。古い環境向けには polyfill が必要です。
5.1 基本的な使い方(実装例)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Node 20+ / Chrome 124+ の実装を前提 async function* numbers() { for (let i = 1; i <= 10; i++) yield i; } // map → filter → take のチェーン例 const result = await numbers() .map(async n => n * 2) // 非同期変換 .filter(async n => n % 3 === 0) // 条件で絞り込み .take(3) // 最初の 3 要素だけ取得 .toArray(); // 配列へ収集 console.log(result); // → [6,12,18] |
map/filterのコールバックは 非同期関数でも可。.take(n)は「最初の n 要素だけ取得」し、残りは評価されません。
5.2 ストリーミング API への適用シナリオ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function* eventStream() { let id = 0; while (true) { await new Promise(r => setTimeout(r, 1000)); yield { id: ++id, type: id % 2 ? 'odd' : 'even', payload: `data-${id}` }; } } // 偶数イベントだけ取得し、最初の5件で停止 const events = await eventStream() .filter(e => e.type === 'even') .take(5) .toArray(); console.log(events); |
- リアルタイム性 と メモリ削減 が同時に得られ、ページングロジックを大幅に簡略化できます。
5.3 互換性チェックと Polyfill の導入
|
1 2 3 4 5 6 7 |
// Feature detection if (!('map' in AsyncIterator.prototype)) { // core-js の提案段階 polyfill を動的インポート(ESM 前提) import('core-js/proposals/iterator-helpers') .then(() => console.log('Async Iterator Helpers がポリフィルされました')); } |
core-js@3のproposals/iterator-helpersは個別にインポート可能です。- Tree‑shaking が働くので、使用しないヘルパーはバンドルに含まれません。
6️⃣ パフォーマンス測定と最適化ポイント
| 観点 | 推奨手法 | 補足 |
|---|---|---|
| レイテンシ | Chrome DevTools の Performance タブ、Node.js の --trace-async-hooks |
await が多いとマイクロタスクが増大 |
| メモリ使用量 | process.memoryUsage()(Node)・performance.memory(ブラウザ) |
大規模ストリームはバッファサイズを意識 |
| スループット | 同一イテレーション内で Promise.all に集約 |
多数の小さな await をまとめるとオーバーヘッド削減 |
6.1 実践的最適化例
|
1 2 3 4 5 6 7 8 9 |
// バッチ取得パターン(5 件ずつ同時リクエスト) async function* batchedFetch(urls, batchSize = 5) { for (let i = 0; i < urls.length; i += batchSize) { const slice = urls.slice(i, i + batchSize); const responses = await Promise.all(slice.map(u => fetch(u))); for (const r of responses) yield await r.json(); } } |
Promise.allにより マイクロタスク数が削減、ネットワーク待ち時間も平行化できます。
6.2 async iterator helpers のベストプラクティス
- 必要最小限のチェーンに留める
map → filter → takeの順序はデータ量を削減できる側から並べると効率的。flatMapは大量要素の展開に注意- 展開先が無限ストリームの場合、メモリリークにつながりやすいので
takeで上限を設定する。
7️⃣ まとめ
| トピック | キーポイント |
|---|---|
| AsyncIterator | next() が必ず Promise を返し、非同期データのイテレーションを統一的に扱える。 |
| async generator / for‑await‑of | 非同期ロジックとループ構文がシームレスに融合し、可読性が向上する。 |
| カスタム実装 | Symbol.asyncIterator を実装すればページングやストリーミングの独自イテレータが数行で作れる。 |
| エラーハンドリング & キャンセル | try/catch で例外捕捉、AbortSignal.throwIfAborted()(最新ランタイム)で安全に中断、finally で必ずリソースを解放。 |
| Async Iterator Helpers (提案) | .map()/.filter()/.take()/.flatMap() が Stage 3 提案として実装されつつあり、チェーンによるストリーム処理が簡潔になる。古い環境は core-js で polyfill。 |
| 互換性 | Chrome 124・Edge 124・Safari 17(プレビュー)・Node.js 20 がネイティブ実装、Feature detection と動的ポリフィルでフォールバック可能。 |
| パフォーマンス | await の頻度削減やバッチ化 (Promise.all) を意識し、DevTools / async‑hooks で測定・チューニングする。 |
実務に活かすポイント
- プロジェクトの対象環境が最新ランタイムをサポートしているなら、for‑await‑ofとカスタムイテレータだけでも多くの非同期データ処理をシンプル化できます。
- さらに Iterator Helpers が利用可能であれば、データ変換・フィルタリング・取得件数制限といった共通ロジックを標準メソッドに置き換え、外部依存(ixjs 等)を削減できます。
- 互換性が必要な場合は feature detection + core‑js polyfill を組み込むことで、古いブラウザや LTS バージョンの Node.js でも同一コードベースを保てます。
以上が、2024 年から 2026 年にかけての JavaScript エコシステムで 安全・効率的に AsyncIterator を活用するための実践ガイドです。ぜひ自プロジェクトへ取り入れ、非同期ストリーム処理を次のレベルへ引き上げてください。