Entendendo Async Iterators
Um async iterator é um objeto que implementa o protocolo assíncrono de iteração, permitindo percorrer dados que chegam de forma assíncrona. Diferentemente dos iteradores comuns, ele retorna uma Promise que resolve para { value, done }.
Para ser um async iterator, um objeto precisa implementar o método [Symbol.asyncIterator](), que retorna o próprio iterator com um método next() que retorna uma Promise. Isso é fundamental quando trabalhamos com fluxos de dados, APIs ou leitura de arquivos que não estão imediatamente disponíveis.
// Exemplo: Iterator assíncrono simples
const asyncIterator = {
counter: 0,
[Symbol.asyncIterator]() {
return this;
},
async next() {
this.counter++;
if (this.counter <= 3) {
return { value: `Item ${this.counter}`, done: false };
}
return { done: true };
}
};
// Usando for await...of
(async () => {
for await (const item of asyncIterator) {
console.log(item); // Item 1, Item 2, Item 3
}
})();
Async Generators na Prática
Um async generator é uma função que combina async e function*, permitindo pausar a execução com await e retornar valores com yield. É a forma mais elegante e legível de criar async iterators.
Generators assíncrono são especialmente úteis para consumir APIs, processar grandes volumes de dados ou implementar padrões de produção-consumo. A função async function* retorna automaticamente um async iterator quando chamada.
// Exemplo: Fetchando dados paginados
async function* fetchPaginatedData(apiUrl, pageSize) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${apiUrl}?page=${page}&limit=${pageSize}`);
const data = await response.json();
if (data.items.length === 0) {
hasMore = false;
} else {
for (const item of data.items) {
yield item;
}
page++;
}
}
}
// Consumindo
(async () => {
for await (const item of fetchPaginatedData('https://api.example.com/users', 10)) {
console.log(item.name);
}
})();
Exemplo com Leitura de Arquivo
// Lendo arquivo linha por linha (Node.js)
const fs = require('fs');
const readline = require('readline');
async function* readFileLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
// Usando
(async () => {
for await (const line of readFileLines('dados.txt')) {
console.log(line);
}
})();
Padrões Avançados e Casos de Uso
Composição e Transformação
Você pode encadear múltiplos async generators para criar pipelines de dados elegantes. Isso é particularmente poderoso para processamento ETL (Extract, Transform, Load).
// Filtrando dados
async function* filter(asyncIterable, predicate) {
for await (const item of asyncIterable) {
if (await predicate(item)) {
yield item;
}
}
}
// Transformando dados
async function* map(asyncIterable, transform) {
for await (const item of asyncIterable) {
yield await transform(item);
}
}
// Usando pipeline
(async () => {
const users = async function* () {
yield { id: 1, name: 'Alice', age: 28 };
yield { id: 2, name: 'Bob', age: 17 };
yield { id: 3, name: 'Charlie', age: 35 };
};
const adults = filter(users(), user => user.age >= 18);
const names = map(adults, user => user.name.toUpperCase());
for await (const name of names) {
console.log(name); // ALICE, CHARLIE
}
})();
Tratamento de Erros
Erros em async generators devem ser tratados com try/catch tanto na função quanto durante a consumição, garantindo robustez.
async function* safeDataFetch(urls) {
for (const url of urls) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
yield await response.json();
} catch (error) {
console.error(`Erro ao buscar ${url}:`, error.message);
yield null; // ou use `continue;` para pular
}
}
}
(async () => {
for await (const data of safeDataFetch(['url1', 'url2', 'url3'])) {
if (data) console.log(data);
}
})();
Performance e Boas Práticas
Async generators são eficientes em memória porque processam um item por vez, ideal para grandes datasets. Nunca coloque toda a lógica assíncrona dentro de um único await no meio do generator — isso pode travar a iteração.
Dica importante: Use
for await...ofapenas quando realmente precisar de assincronicidade. Para arrays normais, continue usandoforou métodos comomap()efilter().
// ❌ Ineficiente: carregando tudo na memória
async function* badGenerator() {
const allData = await fetch('/api/huge-dataset').then(r => r.json());
for (const item of allData) {
yield item;
}
}
// ✅ Eficiente: carregando sob demanda
async function* goodGenerator() {
let page = 1;
let hasData = true;
while (hasData) {
const data = await fetch(`/api/data?page=${page}`).then(r => r.json());
if (data.length === 0) hasData = false;
for (const item of data) {
yield item;
}
page++;
}
}
Conclusão
Os principais aprendizados são: (1) Async iterators e generators oferecem uma forma elegante e eficiente de trabalhar com dados assíncrono em JavaScript, especialmente quando combinados com for await...of; (2) generators assíncrono (async function*) são a abordagem recomendada na maioria dos casos por sua legibilidade e capacidade de pausar/resumir com await e yield; (3) padrões de composição (filter, map, etc.) permitem criar pipelines robustos de processamento de dados sem carregar tudo na memória.