Iteradores Assíncronos para Streaming no SDK TypeScript
Consuma deltas de mensagens com loops for-await em vez de listeners de eventos manuais.
Resumo
O pacote @anthropic-ai/sdk transmite respostas do Claude como uma sequência de eventos, e a maneira moderna de consumi-los é um loop for await...of sobre o objeto de stream retornado por client.messages.stream(...).
Versões mais antigas do SDK e muitos tutoriais configuram o streaming com listeners de eventos no estilo .on('text', ...) , emprestado do padrão EventEmitter do Node. Esse estilo ainda funciona, mas espalha o estado entre closures de callback e torna o tratamento de erros complicado.
Iteradores assíncronos se integram naturalmente ao fluxo de controle normal async/await. Você escreve um loop linear, usa try/catch para erros e break para sair mais cedo exatamente como faria com um array.
O objeto de stream retornado por .stream() é tanto um iterável assíncrono (para a visualização granular evento por evento) quanto um helper com finalMessage() (para a resposta completa montada quando o stream termina).
Esta página contrasta ambos os estilos e percorre um exemplo completo que transmite uma resposta de chat e renderiza deltas de texto incrementalmente.
Receita
Cartão de receita de referência rápida, pronto para copiar e colar.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Escreva um haicai sobre iteradores assíncronos." }],
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
process.stdout.write(event.delta.text);
}
}
const message = await stream.finalMessage();
console.log("\n\nUso:", message.usage);Quando usar isso:
- Você deseja renderizar tokens incrementalmente em uma interface de usuário, CLI ou terminal à medida que eles chegam.
- Você precisa da mensagem final montada (texto, motivo de parada, uso) após a conclusão do stream.
- Você deseja tratamento type-safe de diferentes tipos de blocos de conteúdo sem nomes de eventos manuais.
- Você está escrevendo código de streaming novo e não tem um motivo legado para usar listeners de eventos.
Exemplo Funcional
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function streamChatResponse(userMessage: string): Promise<void> {
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: userMessage }],
});
let currentBlockType: string | null = null;
for await (const event of stream) {
switch (event.type) {
case "content_block_start":
currentBlockType = event.content_block.type;
if (currentBlockType === "text") {
process.stdout.write("Claude: ");
}
break;
case "content_block_delta":
if (event.delta.type === "text_delta") {
// Renderiza deltas de texto incrementalmente à medida que chegam.
process.stdout.write(event.delta.text);
} else if (event.delta.type === "thinking_delta") {
// Deltas de pensamento estendido chegam em seu próprio tipo de bloco.
process.stdout.write(event.delta.thinking);
}
break;
case "content_block_stop":
if (currentBlockType === "text") {
process.stdout.write("\n");
}
currentBlockType = null;
break;
case "message_stop":
// O stream terminou; finalMessage() abaixo é agora rápido.
break;
}
}
// Após o loop, a mensagem completa está disponível sem reler os eventos.
const finalMessage = await stream.finalMessage();
console.log("Motivo da parada:", finalMessage.stop_reason);
console.log("Tokens de entrada:", finalMessage.usage.input_tokens);
console.log("Tokens de saída:", finalMessage.usage.output_tokens);
}
streamChatResponse("Explique iteradores assíncronos em duas frases.").catch((error) => {
console.error("Falha no streaming:", error);
});O que isso demonstra:
- Um único loop linear
for await...ofsubstitui múltiplos callbacks de listener de eventos nomeados. - O estreitamento em
event.typeeevent.delta.typefornece acesso type-safe a variantes detext,thinkinge outras deltas sem necessidade de casting. - Tratamento de erros no estilo
try/catch(aqui,.catch()na função assíncrona externa) cobre todo o stream, não apenas eventos individuais. stream.finalMessage()retorna o objetoMessagecompleto montado após a iteração terminar, sem contabilidade extra.- Variáveis locais como
currentBlockTypevivem no escopo normal do loop em vez de serem transportadas através de closures.
Mergulho Profundo
Como Funciona
client.messages.stream(...)abre uma conexão de server-sent-events para a API Messages e retorna um objetoMessageStreamimediatamente, antes que quaisquer eventos tenham chegado.MessageStreamimplementaSymbol.asyncIterator, entãofor await (const event of stream)puxa eventos da conexão um por um, em ordem, aguardando cada um à medida que se torna disponível.- Cada iteração produz um evento do protocolo de stream bruto:
message_start,content_block_start,content_block_delta,content_block_stop,message_deltaoumessage_stop. - Como o loop é apenas fluxo de controle
async, umreturn,breakou erro lançado dentro dele se propaga da maneira que faria em qualquer outro loopfor await, e a conexão subjacente é limpa. stream.finalMessage()resolve assim que o stream termina, usando um acumulador interno que remonta todos os blocos de conteúdo e deltas em um único objetoMessageidêntico em forma a uma resposta não-streaming.- Você não precisa iterar os eventos se estiver interessado apenas no resultado final: chamar
await stream.finalMessage()diretamente (sem um loopfor await) ainda funciona, pois o SDK impulsiona o stream à conclusão internamente.
Tipos de Eventos em Resumo
| Tipo de evento | Quando dispara | Campos úteis |
|---|---|---|
message_start | Uma vez, no início da resposta | message (id, model, role, conteúdo vazio) |
content_block_start | Uma vez por bloco de conteúdo | index, content_block (type: text, tool_use, thinking) |
content_block_delta | Repetidamente, à medida que os tokens chegam | delta (text_delta, thinking_delta, ou input_json_delta) |
content_block_stop | Uma vez por bloco de conteúdo, quando ele está completo | index |
message_delta | Perto do final, com alterações de nível superior | delta.stop_reason, usage |
message_stop | Uma vez, quando o stream está totalmente concluído | (sem payload) |
Iteradores Assíncronos vs. Listeners de Eventos Manuais
// Estilo antigo: configuração manual de listener de eventos.
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Olá, Claude" }],
});
stream.on("text", (textDelta) => {
process.stdout.write(textDelta);
});
stream.on("error", (error) => {
console.error("Erro no stream:", error);
});
stream.on("end", () => {
console.log("\nConcluído.");
});
// Estilo moderno: um loop de iterador assíncrono, mostrado na Receita e no Exemplo Funcional acima.O estilo de listener ainda é enviado no SDK para compatibilidade com versões anteriores, mas tem três desvantagens práticas em comparação com for await...of:
- Fluxo de controle espalhado. Sucesso, erro e conclusão vivem em callbacks separados, então não há um único lugar para raciocinar sobre "o que acontece durante este stream".
- Limpeza complicada. Sair de um stream mais cedo (por exemplo, depois que o usuário cancela uma solicitação) significa chamar manualmente
stream.abort()e desregistrar listeners, em vez de apenasbreakem um loop. - Tipagem mais fraca no local de chamada. Nomes de eventos como
"text"são strings que você precisa acertar, em vez de valoresevent.typeque o TypeScript refina para você dentro do loop.
Notas do TypeScript
import type { MessageStreamEvent } from "@anthropic-ai/sdk/resources/messages";
function handleEvent(event: MessageStreamEvent): void {
// event.type refina a união, então event.delta e event.content_block
// só são acessíveis com os campos que essa variante realmente tem.
if (event.type === "content_block_delta") {
if (event.delta.type === "text_delta") {
console.log(event.delta.text); // string, sem necessidade de cast
} else if (event.delta.type === "input_json_delta") {
console.log(event.delta.partial_json); // string, fragmento de entrada de ferramenta
}
}
}O SDK envia suas próprias definições de tipo, então cada evento e bloco de conteúdo é uma união discriminada com chave em type. Refinar com if/switch em event.type (e event.delta.type um nível abaixo) fornece tratamento exaustivo e verificado pelo compilador em vez de payloads de callback fracamente tipados.
Parâmetros e Valores de Retorno
| Parâmetro | Tipo | Descrição |
|---|---|---|
model | string | ID do modelo, por exemplo, "claude-sonnet-5". |
max_tokens | number | Limite superior de tokens gerados para esta resposta. |
messages | MessageParam[] | Histórico da conversa passado para .stream(), com a mesma forma da chamada .create() não-streaming. |
stream.finalMessage() | Promise<Message> | Resolve para a mensagem totalmente montada após o término do stream. |
stream.abort() | () => void | Cancela a conexão subjacente; seguro para chamar de dentro ou de fora do loop. |
Armadilhas
- Iterar o stream duas vezes. Um
MessageStreamsó pode ser consumido uma vez; chamá-lo comfor awaituma segunda vez não produz nada. Correção: capture o que você precisa (texto, uso, chamadas de ferramenta) durante a única passagem, ou chamestream.finalMessage()se você precisar apenas do resultado final. - Esquecer de refinar
event.delta.type. Tratar cadacontent_block_deltacomo um delta de texto falha quando umthinking_deltaouinput_json_deltachega, pois essas variantes usam nomes de campos diferentes. Correção: sempre verifiqueevent.delta.typeantes de lerevent.delta.text. - Misturar listeners
.on()com um loopfor awaitno mesmo stream. Ambos consomem os mesmos eventos subjacentes, então combiná-los leva a dados perdidos ou duplicados. Correção: escolha um estilo por stream; use o iterador assíncrono, a menos que você tenha um motivo específico para manter os listeners. - Não tratar erros de stream. Uma rejeição não tratada de uma conexão quebrada ou um erro de API no meio do stream derrubará um processo Node se nada o aguardar ou capturar. Correção: envolva o loop (ou a função assíncrona de chamada) em
try/catch, ou anexe um.catch()à promessa externa. - Chamar
finalMessage()antes que o loop termine. Se você chamarstream.finalMessage()concorrentemente enquanto ainda itera em outra parte do código, você pode acabar competindo com o mesmo stream. Correção: chamefinalMessage()após a conclusão do loopfor await, ou pule o loop inteiramente e chamefinalMessage()apenas se você não precisar de atualizações por token. - Assumir que
content_block_startsempre significa texto. Blocos de uso de ferramenta e blocos de pensamento também disparamcontent_block_start, com umcontent_block.typediferente. Correção: verifiquecontent_block.typeantes de assumir que o bloco étext.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
Listeners de eventos manuais (.on('text', ...)) | Você está mantendo código antigo já construído em torno de listeners, ou você precisa de múltiplos assinantes independentes em um stream. | Você está escrevendo código novo; o iterador assíncrono é mais simples e menos propenso a erros. |
client.messages.create(...) não-streaming | A resposta é curta, a latência para o primeiro token não importa, e você só quer o texto final. | Você precisa renderizar a saída parcial à medida que ela chega, por exemplo, uma interface de chat. |
| Server-sent events encaminhados para um cliente do navegador | Você está encaminhando um stream do lado do Node para um frontend via HTTP. | Você está consumindo o stream inteiramente no lado do servidor; não há necessidade de um salto de transporte extra. |
FAQs
Qual é a diferença entre iterar o stream e chamar stream.finalMessage()?
- Iterar (
for await...of) fornece cada evento individual à medida que ele chega, útil para renderizar saída parcial. stream.finalMessage()resolve uma vez, após o término do stream, com aMessagecompleta montada.- Você pode fazer ambos: iterar para renderização incremental e, em seguida, chamar
finalMessage()depois para obter uso/stop_reason sem ter que reanalisar os eventos você mesmo.
Posso usar for await...of sem nunca chamar .on()?
Sim. O iterador assíncrono é a maneira primária e idiomática de consumir um stream em versões atuais do SDK. Os listeners de eventos são mantidos para compatibilidade com versões anteriores, não porque sejam obrigatórios.
Por que event.delta tem formas diferentes no loop?
Os eventos content_block_delta carregam um campo delta que é, por si só, uma união discriminada: text_delta, thinking_delta ou input_json_delta, dependendo de que tipo de bloco de conteúdo está sendo transmitido. Verifique event.delta.type antes de ler campos específicos do tipo como text ou partial_json.
Como interrompo um stream mais cedo, como quando um usuário cancela uma solicitação?
const stream = client.messages.stream({ /* ... */ });
const timeout = setTimeout(() => stream.abort(), 5000);
for await (const event of stream) {
// ... lidar com eventos
}
clearTimeout(timeout);Chamar stream.abort() fecha a conexão subjacente; o loop for await então sai sem lançar uma exceção.
Este padrão funciona em runtimes de edge, não apenas em Node.js?
Sim. O SDK é construído sobre fetch e iteradores assíncronos padrão, ambos disponíveis em runtimes de edge (Vercel Edge, Cloudflare Workers), bem como em Node.js.
O que acontece se eu tentar iterar o mesmo objeto de stream duas vezes?
O segundo loop for await não produz eventos, pois o stream já foi totalmente consumido. Capture quaisquer dados que você precise durante a primeira passagem, ou emita a solicitação novamente com .stream(...) se precisar de um stream novo.
Como obtenho apenas o texto puro enquanto ele é transmitido, sem lidar com todos os tipos de evento?
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Oi" }],
});
for await (const text of stream.textStream ?? []) {
process.stdout.write(text);
}Se a sua versão do SDK expõe uma propriedade de conveniência iterável assíncrona textStream, ela filtra os eventos para apenas deltas de texto. Caso contrário, filtre os eventos content_block_delta onde delta.type === "text_delta" você mesmo, como mostrado no Exemplo Funcional.
message_delta é o mesmo que content_block_delta?
Não. content_block_delta carrega conteúdo incremental (texto, pensamento ou JSON de uso de ferramenta) para um bloco de conteúdo. message_delta carrega alterações de nível superior na mensagem como um todo, como o stop_reason final e o usage cumulativo, e dispara perto do final do stream.
Preciso de try/catch em torno do loop for await?
Sim, para código de produção. Erros de rede, limites de taxa ou erros de API durante o streaming surgem como exceções lançadas pelo loop (ou uma promessa rejeitada de finalMessage()), portanto, envolva a chamada em try/catch ou anexe um .catch() à função assíncrona envolvente.
Posso combinar o uso de ferramentas e o streaming de texto no mesmo loop?
Sim. Uma única resposta pode incluir blocos de conteúdo text e tool_use. Verifique content_block.type em content_block_start e event.delta.type em content_block_delta para ramificar entre deltas de texto e input_json_delta (argumentos parciais de chamada de ferramenta) dentro do mesmo loop.
Por que meu output de thinking_delta é diferente de text_delta?
Os eventos thinking_delta carregam seu conteúdo sob event.delta.thinking, não event.delta.text. Eles aparecem apenas quando o pensamento estendido é habilitado para a solicitação e pertencem a um bloco de conteúdo thinking em vez de um bloco text.
Usar iteradores assíncronos muda como configuro o cliente ou a solicitação?
Não. Você ainda constrói o cliente com new Anthropic() e passa os mesmos campos model, max_tokens e messages. A única diferença é chamar .stream(...) em vez de .create(...), e então consumir o resultado com for await em vez de aguardar uma única resposta.
Relacionados
- Noções Básicas do SDK TypeScript/JavaScript - configuração do cliente e noções básicas de solicitação nas quais esta página se baseia.
- O Modelo Mental do SDK TypeScript da Anthropic - como solicitações, respostas e streams se encaixam conceitualmente.
- Referência de Tipos do SDK TypeScript para Blocos de Conteúdo - referência completa de tipos para
TextBlock,ToolUseBlockeThinkingBlock. - Construindo um Endpoint de Chat com Streaming usando Next.js e o SDK TypeScript - aplica este padrão de streaming dentro de um manipulador de rota Next.js.
- Padrões de Retry, Timeout e AbortController no SDK TypeScript - cancelando e tentando streams com segurança.
Versões da Pilha: Escrito contra a linha de modelos Claude atual a partir de ~junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão), e Claude Haiku 4.5 - e o SDK oficial
@anthropic-ai/sdkTypeScript (último lançamento). Nomes de modelos, versões de SDK e preços mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs antes de confiar neles.