Асинхронные итераторы

Асинхронные итераторы #

Асинхронный итератор реализует [Symbol.asyncIterator]() и возвращает объект, у которого next() отдаёт Promise на { value, done }. Цикл for await...of и async-генераторы async function* — основной способ последовательно обрабатывать данные из промисов и fetch без ручной цепочки then.


for await...of #

Перебирает async iterable — каждый шаг ждёт разрешения Promise:

async function loadOrderStatuses(ids) {
  const results = []
  for (const id of ids) {
    const res = await fetch(`/api/orders/${id}`)
    results.push(await res.json())
  }
  return results
}

Когда источник уже async iterable:

const statuses = {
  async *[Symbol.asyncIterator]() {
    yield await Promise.resolve('pending')
    yield await Promise.resolve('shipped')
    yield await Promise.resolve('delivered')
  },
}

for await (const status of statuses) {
  console.log(status)
}

Async-генератор async function* #

Сочетает yield с await внутри тела:

async function fetchOrderPage(page) {
  const res = await fetch(`/api/orders?page=${page}`)
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}

async function* allOrders() {
  let page = 1
  while (true) {
    const { items, hasMore } = await fetchOrderPage(page)
    yield* items
    if (!hasMore) return
    page++
  }
}

for await (const order of allOrders()) {
  console.log(order.id)
}

Каждый yield приостанавливает генератор до следующего запроса next() в for await — память не заполняется всем каталогом сразу.


Отличие от синхронного генератора #

Синхронный function*Async async function*
next() возвращает{ value, done }Promise<{ value, done }>
await в теленельзяможно
Циклfor...offor await...of
SymbolSymbol.iteratorвстроенный async iterator

Синхронный генератор с промисами внутри без async потребует ручного await на каждом next() — async-генератор скрывает эту обёртку.


Ручной async-итератор #

function createDeliveryEvents(socket) {
  const queue = []
  let resolveWait = null

  socket.on('event', (ev) => {
    if (resolveWait) {
      resolveWait({ value: ev, done: false })
      resolveWait = null
    } else {
      queue.push(ev)
    }
  })

  return {
    [Symbol.asyncIterator]() {
      return {
        async next() {
          if (queue.length) {
            return { value: queue.shift(), done: false }
          }
          return new Promise((resolve) => {
            resolveWait = (result) => resolve(result)
          })
        },
      }
    },
  }
}

На практике чаще используют async function* или готовые абстракции (ReadableStream и т.д.).


Обработка ошибок #

Ошибка внутри async-генератора прерывает for await...of, если не перехвачена:

async function* flakyCheckout() {
  yield await loadCart()
  throw new Error('оплата недоступна')
  yield await confirmOrder()
}

try {
  for await (const step of flakyCheckout()) {
    console.log(step)
  }
} catch (e) {
  console.error(e.message)
}

Паттерн как в try/catch с async/await — один try вокруг цикла или внутри итерации.


Параллельность и осторожность #

for await...of выполняет итерации последовательно. Для параллельной загрузки страниц — Promise.all или ограниченный пул:

async function* expressSlots(ids, concurrency = 2) {
  const pending = new Set()

  for (const id of ids) {
    const task = fetch(`/api/slots/${id}`).then((r) => r.json())
    pending.add(task)
    task.finally(() => pending.delete(task))

    if (pending.size >= concurrency) {
      yield await Promise.race(pending)
    }
  }

  while (pending.size) {
    yield await Promise.race(pending)
  }
}

Выбор между последовательным async-итератором и параллельным batch — вопрос нагрузки на API и порядка результатов.


Практика: поток обновлений доставки #

async function* trackShipment(trackingId) {
  let lastEvent = null

  while (true) {
    const res = await fetch(`/api/tracking/${trackingId}`)
    const { events, completed } = await res.json()
    const fresh = events.filter((e) => e.id !== lastEvent?.id)

    for (const event of fresh) {
      lastEvent = event
      yield event
    }

    if (completed) return
    await new Promise((r) => setTimeout(r, 5000))
  }
}

for await (const event of trackShipment('TRK-9X2')) {
  console.log(event.label)
}
async function collectPlaylistFromApi(slug) {
  const tracks = []
  for await (const track of fetchTracksPages(slug)) {
    tracks.push(track)
    if (tracks.length >= 50) break
  }
  return tracks
}

async function* fetchTracksPages(slug) {
  let cursor = null
  do {
    const q = cursor ? `?cursor=${cursor}` : ''
    const data = await fetch(`/api/playlists/${slug}/tracks${q}`).then((r) => r.json())
    yield* data.items
    cursor = data.nextCursor
  } while (cursor)
}

Итог #

  • Async iterable: next() → Promise<{ value, done }>; обход — for await...of.
  • async function* объединяет yield и await для ленивых асинхронных последовательностей.
  • Подходит для пагинации API, потоков статусов и пошагового чекаута с сетевыми вызовами.
  • Для параллельных запросов комбинируйте с Promise.all; async-итератор по умолчанию последователен.

См. также #