Iteradores asíncronos para streaming en el SDK de TypeScript
Consume deltas de mensajes con bucles for-await en lugar de oyentes de eventos manuales.
Resumen
El paquete @anthropic-ai/sdk transmite respuestas de Claude como una secuencia de eventos, y la forma moderna de consumirlos es un bucle for await...of sobre el objeto de stream devuelto por client.messages.stream(...).
Las versiones anteriores del SDK y muchos tutoriales configuran el streaming con oyentes de eventos del tipo .on('text', ...) tomados del patrón EventEmitter de Node. Ese estilo todavía funciona, pero dispersa el estado entre cierres de callback y hace que el manejo de errores sea incómodo.
Los iteradores asíncronos se integran de forma natural en el flujo de control normal de async/await. Escribes un bucle lineal, usas try/catch para errores y break para salir anticipadamente exactamente como lo harías con un array.
El objeto de stream devuelto por .stream() es tanto un iterable asíncrono (para la vista granular evento por evento) como una utilidad con finalMessage() (para la respuesta completa ensamblada una vez que el stream finaliza).
Esta página contrasta ambos estilos y recorre un ejemplo completo que transmite una respuesta de chat y renderiza deltas de texto incrementalmente.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
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: "Escribe un haiku sobre iteradores así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);Cuándo usar esto:
- Quieres renderizar tokens incrementalmente en una UI, CLI o terminal a medida que llegan.
- Necesitas el mensaje final ensamblado (texto, razón de parada, uso) después de que el stream se complete.
- Quieres manejo seguro de tipos para diferentes tipos de bloques de contenido sin nombres de eventos manuales.
- Estás escribiendo código nuevo para streaming y no tienes una razón heredada para usar oyentes de eventos.
Ejemplo 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 a medida que llegan.
process.stdout.write(event.delta.text);
} else if (event.delta.type === "thinking_delta") {
// Los deltas de pensamiento extendido llegan en su propio tipo de bloque.
process.stdout.write(event.delta.thinking);
}
break;
case "content_block_stop":
if (currentBlockType === "text") {
process.stdout.write("\n");
}
currentBlockType = null;
break;
case "message_stop":
// El stream ha finalizado; finalMessage() a continuación es rápido.
break;
}
}
// Después del bucle, el mensaje completo está disponible sin volver a leer eventos.
const finalMessage = await stream.finalMessage();
console.log("Razón de parada:", finalMessage.stop_reason);
console.log("Tokens de entrada:", finalMessage.usage.input_tokens);
console.log("Tokens de salida:", finalMessage.usage.output_tokens);
}
streamChatResponse("Explica los iteradores asíncronos en dos frases.").catch((error) => {
console.error("El streaming falló:", error);
});Lo que esto demuestra:
- Un único bucle lineal
for await...ofreemplaza múltiples callbacks de oyentes de eventos con nombre. - El estrechamiento de
event.typeyevent.delta.typeproporciona acceso seguro a tipos para variantes detext,thinkingy otras sin necesidad de conversiones. - El manejo de errores estilo
try/catch(aquí,.catch()en la función asíncrona externa) cubre todo el stream, no solo eventos individuales. stream.finalMessage()devuelve el objetoMessagecompleto ensamblado después de que la iteración finaliza, sin contabilidad adicional.- Las variables locales como
currentBlockTyperesiden en el ámbito normal del bucle en lugar de ser transportadas a través de cierres.
Análisis en profundidad
Cómo funciona
client.messages.stream(...)abre una conexión de eventos enviados por el servidor (server-sent events) a la API de Mensajes y devuelve un objetoMessageStreaminmediatamente, antes de que hayan llegado eventos.MessageStreamimplementaSymbol.asyncIterator, por lo quefor await (const event of stream)extrae eventos de la conexión uno a la vez, en orden, esperando cada uno a medida que está disponible.- Cada iteración produce un evento del protocolo de stream sin procesar:
message_start,content_block_start,content_block_delta,content_block_stop,message_deltaomessage_stop. - Dado que el bucle es solo flujo de control
async, unreturn,breako error lanzado dentro de él se propaga de la manera que lo haría en cualquier otro buclefor await, y la conexión subyacente se limpia. stream.finalMessage()se resuelve una vez que el stream ha finalizado, utilizando un acumulador interno que reensambla todos los bloques de contenido y deltas en un único objetoMessageidéntico en forma a una respuesta no streaming.- No tienes que iterar los eventos en absoluto si solo te importa el resultado final: llamar directamente a
await stream.finalMessage()(sin un buclefor await) todavía funciona, ya que el SDK impulsa el stream a completarse internamente.
Tipos de eventos de un vistazo
| Tipo de evento | Cuándo se dispara | Campos útiles |
|---|---|---|
message_start | Una vez, al inicio de la respuesta | message (id, modelo, rol, contenido vacío) |
content_block_start | Una vez por bloque de contenido | index, content_block (tipo: text, tool_use, thinking) |
content_block_delta | Repetidamente, a medida que llegan tokens | delta (text_delta, thinking_delta, o input_json_delta) |
content_block_stop | Una vez por bloque de contenido, cuando está completo | index |
message_delta | Cerca del final, con cambios de nivel superior | delta.stop_reason, usage |
message_stop | Una vez, cuando el stream está completamente terminado | (sin carga útil) |
Iteradores asíncronos vs. Oyentes de eventos manuales
// Estilo antiguo: configuración manual de oyentes de eventos.
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hola, Claude" }],
});
stream.on("text", (textDelta) => {
process.stdout.write(textDelta);
});
stream.on("error", (error) => {
console.error("Error del stream:", error);
});
stream.on("end", () => {
console.log("\nTerminado.");
});
// Estilo moderno: un bucle de iterador asíncrono, mostrado en la Receta y el Ejemplo funcional arriba.El estilo de oyente todavía se envía en el SDK para compatibilidad con versiones anteriores, pero tiene tres desventajas prácticas frente a for await...of:
- Flujo de control disperso. El éxito, el error y la finalización viven cada uno en un callback separado, por lo que no hay un lugar único para razonar sobre "qué sucede durante este stream".
- Limpieza incómoda. Salir de un stream anticipadamente (por ejemplo, después de que el usuario cancele una solicitud) significa llamar manualmente a
stream.abort()y anular el registro de oyentes, en lugar de simplementebreaken un bucle. - Tipado más débil en el sitio de llamada. Nombres de eventos como
"text"son cadenas que debes escribir correctamente, en comparación con los valoresevent.typeque TypeScript reduce por ti dentro del bucle.
Notas de TypeScript
import type { MessageStreamEvent } from "@anthropic-ai/sdk/resources/messages";
function handleEvent(event: MessageStreamEvent): void {
// event.type reduce la unión, por lo que event.delta y event.content_block
// solo son accesibles con los campos que esa variante realmente tiene.
if (event.type === "content_block_delta") {
if (event.delta.type === "text_delta") {
console.log(event.delta.text); // string, no se necesita conversión
} else if (event.delta.type === "input_json_delta") {
console.log(event.delta.partial_json); // string, fragmento de entrada de herramienta
}
}
}El SDK envía sus propias definiciones de tipo, por lo que cada evento y bloque de contenido es una unión discriminada indexada por type. Reducir con if/switch en event.type (y event.delta.type un nivel más abajo) te proporciona un manejo exhaustivo y verificado por el compilador en lugar de cargas útiles de callback débilmente tipadas.
Parámetros y valores de retorno
| Parámetro | Tipo | Descripción |
|---|---|---|
model | string | ID del modelo, por ejemplo, "claude-sonnet-5". |
max_tokens | number | Límite superior de tokens generados para esta respuesta. |
messages | MessageParam[] | Historial de conversación pasado a .stream(), con la misma forma que la llamada .create() no streaming. |
stream.finalMessage() | Promise<Message> | Se resuelve con el mensaje completamente ensamblado después de que el stream finaliza. |
stream.abort() | () => void | Cancela la conexión subyacente; es seguro llamarlo desde dentro o fuera del bucle. |
Trampas comunes
- Iterar el stream dos veces. Un
MessageStreamsolo se puede consumir una vez; llamar afor awaitsobre él una segunda vez no produce nada. Solución: captura lo que necesites (texto, uso, llamadas a herramientas) durante el único pase, o llama astream.finalMessage()si solo necesitas el resultado final. - Olvidar reducir
event.delta.type. Tratar cadacontent_block_deltacomo un delta de texto falla cuando llega unthinking_deltaoinput_json_delta, ya que esas variantes usan nombres de campo diferentes. Solución: siempre verificaevent.delta.typeantes de leerevent.delta.text. - Mezclar oyentes
.on()con un buclefor awaiten el mismo stream. Ambos consumen los mismos eventos subyacentes, por lo que combinarlos conduce a datos perdidos o duplicados. Solución: elige un estilo por stream; usa el iterador asíncrono a menos que tengas una razón específica para mantener los oyentes. - No manejar errores del stream. Una excepción no controlada de una conexión rota o un error de API a mitad del stream hará que un proceso de Node falle si nada lo espera o lo captura. Solución: envuelve el bucle (o la función asíncrona que llama) en
try/catch, o adjunta un.catch()a la función asíncrona externa. - Llamar a
finalMessage()antes de que termine el bucle. Si llamas astream.finalMessage()concurrentemente mientras iteras en otra parte del código, puedes terminar compitiendo por el mismo stream. Solución: llama afinalMessage()después de que el buclefor awaitse complete, o omite el bucle por completo y solo llama afinalMessage()si no necesitas actualizaciones por token. - Asumir que
content_block_startsiempre significa texto. Los bloques de uso de herramientas y los bloques de pensamiento también disparancontent_block_start, con uncontent_block.typediferente. Solución: verificacontent_block.typeantes de asumir que el bloque estext.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
Oyentes de eventos manuales (.on('text', ...)) | Estás manteniendo código antiguo ya construido alrededor de oyentes, o necesitas múltiples suscriptores independientes en un stream. | Estás escribiendo código nuevo; el iterador asíncrono es más simple y menos propenso a errores. |
client.messages.create(...) no streaming | La respuesta es corta, la latencia al primer token no importa y solo quieres el texto final. | Necesitas renderizar salida parcial a medida que llega, por ejemplo, una UI de chat. |
| Server-sent events reenviados a un cliente del navegador | Estás reenviando un stream del lado de Node a un frontend a través de HTTP. | Estás consumiendo el stream completamente en el lado del servidor; no hay necesidad de un salto de transporte adicional. |
Preguntas frecuentes
¿Cuál es la diferencia entre iterar el stream y llamar a stream.finalMessage()?
- Iterar (
for await...of) te da cada evento individual a medida que llega, útil para renderizar salida parcial. stream.finalMessage()se resuelve una vez, después de que el stream finaliza, con elMessagecompleto ensamblado.- Puedes hacer ambas cosas: iterar para renderizado incremental, y luego llamar a
finalMessage()después para obtener el uso/stop_reason sin tener que volver a analizar los eventos tú mismo.
¿Puedo usar for await...of sin llamar nunca a .on()?
Sí. El iterador asíncrono es la forma principal e idiomática de consumir un stream en las versiones actuales del SDK. Los oyentes de eventos se mantienen por compatibilidad con versiones anteriores, no porque sean necesarios.
¿Por qué event.delta tiene formas diferentes en el bucle?
Los eventos content_block_delta llevan un campo delta que es en sí mismo una unión discriminada: text_delta, thinking_delta o input_json_delta, dependiendo de qué tipo de bloque de contenido se esté transmitiendo. Verifica event.delta.type antes de leer campos específicos del tipo como text o partial_json.
¿Cómo detengo un stream anticipadamente, como cuando un usuario cancela una solicitud?
const stream = client.messages.stream({ /* ... */ });
const timeout = setTimeout(() => stream.abort(), 5000);
for await (const event of stream) {
// ... manejar eventos
}
clearTimeout(timeout);Llamar a stream.abort() cierra la conexión subyacente; el bucle for await luego sale sin lanzar una excepción.
¿Este patrón funciona en entornos edge, no solo en Node.js?
Sí. El SDK se basa en fetch e iteradores asíncronos estándar, ambos disponibles en entornos edge (Vercel Edge, Cloudflare Workers) así como en Node.js.
¿Qué sucede si intento iterar el mismo objeto de stream dos veces?
El segundo bucle for await no produce eventos, ya que el stream ya se ha consumido por completo. Captura cualquier dato que necesites durante el primer pase, o vuelve a emitir la solicitud con .stream(...) nuevamente si necesitas un stream nuevo.
¿Cómo obtengo solo el texto plano a medida que se transmite, sin manejar todos los tipos de eventos?
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hola" }],
});
for await (const text of stream.textStream ?? []) {
process.stdout.write(text);
}Si tu versión del SDK expone una propiedad de conveniencia iterable asíncrona textStream, filtra los eventos a solo deltas de texto. De lo contrario, filtra los eventos content_block_delta donde delta.type === "text_delta" tú mismo, como se muestra en el Ejemplo funcional.
¿Es message_delta lo mismo que content_block_delta?
No. content_block_delta transporta contenido incremental (texto, pensamiento o JSON de uso de herramientas) para un bloque de contenido. message_delta transporta cambios de nivel superior al mensaje en su conjunto, como el stop_reason final y el usage acumulado, y se dispara cerca del final del stream.
¿Necesito try/catch alrededor del bucle for await?
Sí, para código de producción. Los errores de red, los límites de tasa o los errores de API durante el streaming aparecen como excepciones lanzadas desde el bucle (o una promesa rechazada de finalMessage()), por lo que envuelve la llamada en try/catch o adjunta un .catch() a la función asíncrona envolvente.
¿Puedo combinar el uso de herramientas y el streaming de texto en el mismo bucle?
Sí. Una única respuesta puede incluir bloques de contenido text y tool_use. Verifica content_block.type en content_block_start y event.delta.type en content_block_delta para ramificarte entre deltas de texto y input_json_delta (argumentos parciales de llamada a herramienta) dentro del mismo bucle.
¿Por qué mi salida de thinking_delta es diferente de text_delta?
Los eventos thinking_delta llevan su contenido bajo event.delta.thinking, no event.delta.text. Solo aparecen cuando el pensamiento extendido está habilitado para la solicitud y pertenecen a un bloque de contenido thinking en lugar de a un bloque text.
¿Usar iteradores asíncronos cambia cómo configuro el cliente o la solicitud?
No. Todavía construyes el cliente con new Anthropic() y pasas los mismos campos model, max_tokens y messages. La única diferencia es llamar a .stream(...) en lugar de .create(...), y luego consumir el resultado con for await en lugar de esperar una única respuesta.
Relacionado
- Conceptos básicos del SDK de TypeScript/JavaScript - configuración del cliente y conceptos básicos de solicitudes en los que se basa esta página.
- El modelo mental del SDK de TypeScript de Anthropic - cómo encajan conceptualmente las solicitudes, respuestas y streams.
- Referencia de tipos del SDK de TypeScript para bloques de contenido - referencia completa de tipos para
TextBlock,ToolUseBlockyThinkingBlock. - Creación de un endpoint de chat en streaming con Next.js y el SDK de TypeScript - aplica este patrón de streaming dentro de un manejador de rutas de Next.js.
- Patrones de reintento, tiempo de espera y AbortController en el SDK de TypeScript - cancelación y reintento seguro de streams.
Versiones de pila: Escrito contra la línea de modelos de Claude actual a partir de ~junio de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (el predeterminado), y Claude Haiku 4.5 - y el SDK oficial de TypeScript
@anthropic-ai/sdk(última versión). Los nombres de modelos, versiones de SDK y precios cambian rápidamente; verifica los detalles actuales en platform.claude.com/docs antes de confiar en ellos.