Cómo manejar errores en agentes de IA usando TypeScript
Error handling en agentes de IA: TypeScript te obliga a hacerlo bien
Tiempo estimado de lectura: 3 min
Ideas clave
- Evita try/catch genéricos: ocultan la causa raíz y llevan a reintentos inútiles o a falsos positivos.
- Usa Result<T, E>: convierte errores en valores tipados que el orquestador y el LLM pueden razonar.
- Forzar exhaustividad con never: el compilador impide desplegar manejadores incompletos.
- Persistir errores en memoria: guarda fallos estructurados en la memoria episódica para decisiones posteriores.
- Instrumenta y prueba: telemetría, tests y SLAs para garantizar conducta operativa.
Tabla de contenidos
- Introducción
- Resumen rápido (lectores con prisa)
- Por qué los try/catch genéricos matan tus agentes
- Result<T, E>: convertir errores en datos con contrato
- never y exhaustive checks: el compilador como guardarraíl
- Integración práctica con memoria y orquestación
- Operacional: telemetría, tests y SLAs
- Cierre operativo
- Dominicode Labs
- FAQ
Introducción
Error handling en agentes de IA: TypeScript te obliga a hacerlo bien. Dilo alto. Si tu agente llama herramientas, escribe datos o encadena flujos, el modo en que tratas los fallos decide si tendrás un sistema autocorrectivo o un monstruo que consume tokens y rompe tablas.
Los try/catch genéricos son la forma más rápida de cegar a un agente. TypeScript, usado con disciplina, no te da magia: te da garantías. Garantías que convierten excepciones impredecibles en valores tipados que el agente puede razonar, registrar y recuperar.
Resumen rápido (lectores con prisa)
Qué: Trata errores como valores con Result<T,E> en lugar de throw.
Cuándo: Siempre que tu agente llame herramientas, escriba en DB o coordine flujos.
Por qué importa: Evita reintentos ciegos, reduce tokens gastados y hace al agente reparable.
Cómo funciona: Devuelve {ok: true|false} con error tipado y usa checks exhaustivos con never para obligar a manejar nuevos casos.
Por qué los try/catch genéricos matan tus agentes
Un humano reintenta después de un HTTP 500. Un agente no. Cuando un LLM recibe solo “Error executing tool”, pierde la causa raíz. ¿Duplicado en la DB? ¿Timeout? ¿Bad payload? Sin esa información, el agente hará una de dos cosas útiles para nadie: reintentar la misma acción hasta agotar tu presupuesto o “alucinar” que la acción fue exitosa para seguir el flujo.
En sistemas agénticos cada fallo es una señal. Ocultarla con un mensaje opaco equivale a apagar los sensores del coche mientras conduces rápido.
Result<T, E>: convertir errores en datos con contrato
La respuesta práctica es sencilla: deja de tirar (throw) y empieza a devolver. El patrón Result<T, E> transforma errores en valores discriminados que obligan al consumidor a lidiar con ellos.
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
Ejemplo de herramienta:
async function createUser(email: string): Promise<Result<User, 'DUPLICATE_EMAIL' | 'DB_TIMEOUT' | 'INVALID_SCHEMA'>> {
// nunca throw; siempre retorna ok: true | ok: false
}
¿Qué cambia? Ahora el orquestador recibe información accionable. Si ok: false y error === ‘DUPLICATE_EMAIL’, inyectas esa razón en la memoria episódica y el LLM pregunta por otro email en vez de repetir la misma INSERT. Transformas ruido en contexto.
Documentación útil sobre Result (concepto inspirado en Rust): Documentación útil sobre Result (concepto inspirado en Rust)
never y exhaustive checks: el compilador como guardarraíl
Pasa esto en equipos: alguien añade un nuevo error y olvida manejarlo. Resultado: el agente se comporta de forma impredecible en producción. Aquí entra el patrón de comprobación exhaustiva con never.
Define tus errores como unión discriminada:
type AgentError =
| { type: 'DatabaseError'; code: number }
| { type: 'AuthError'; reason: string }
| { type: 'ValidationError'; issues: string[] };
Y formatea para el LLM con un switch que fuerza exhaustividad:
function formatErrorForLLM(error: AgentError): string {
switch (error.type) {
case 'DatabaseError': return `DB failure (${error.code}). No retry.`;
case 'AuthError': return `Auth failed: ${error.reason}. Re-auth required.`;
case 'ValidationError': return `Invalid: ${error.issues.join(', ')}. Fix and retry.`;
default:
const _exhaustive: never = error;
return _exhaustive;
}
}
Si alguien añade { type: 'RateLimit' } a AgentError y no actualiza este switch, TypeScript fallará en compilación. Lee sobre exhaustiveness checks: Lee sobre exhaustiveness checks
Eso no es paranoia: es disciplina. Tu CI/CD no permitirá desplegar un agente que no explica cada fallo.
Integración práctica con memoria y orquestación
Errores tipados deben viajar junto con resultados hacia la memoria episódica. Diseño minimalista:
- La tool retorna Result.
- El orquestador persiste tanto éxitos como fallos en la memoria episódica (Redis) y semántica (vector DB si aplica).
- El prompt incluye el fallo estructurado antes de la siguiente llamada al LLM.
- El LLM decide la acción (retry con modificación, pedir datos al usuario, escalar).
Además: valida entradas con Zod/JSON Schema antes de llamar a la tool. Un buen schema reduce la superficie de errores y hace que los fallos restantes sean reales y manejables.
Operacional: telemetría, tests y SLAs
No sirve con tipos si no lo verificas. Tienes que medir:
- Retries por tipo de error (P50/P95).
- Frecuencia de errores no mapeados (build-breakers).
- Tokens gastados en reintentos.
- Casos escalados a humano.
Incluye tests que simulen fallos de DB, timeouts y errores de validación. Tu pipeline debe romper si un nuevo error llega a producción sin un handler en el switch exhaustivo.
Cierre operativo
El try/catch genérico es cómodo. También es la razón por la que tu agente se convierte en una caja negra con facturas altas y datos rotos. Convertir errores en valores tipados (Result<T,E>) y forzar comprobaciones exhaustivas con never no es estilo: es seguridad operativa.
Aplica esto ya, instrumenta telemetría y haz que tu build falle si alguien olvida un caso de error. En el próximo artículo mostraremos cómo serializar esos errores en memoria episódica y usar feedback estructurado para que el agente aprenda a autocorregirse sin intervención humana.
Dominicode Labs
Para equipos que diseñan agentes y pipelines de orquestación, es útil contar con recursos y experimentos que muestren patrones de integración entre memoria episódica y errores tipados. Continúa explorando técnicas y prototipos en Dominicode Labs para ver implementaciones prácticas y ejemplos aplicados a agentes y workflows.
FAQ
- ¿Por qué no debo usar try/catch genéricos en agentes?
- ¿Qué es Result<T,E> y cómo cambia la arquitectura?
- ¿Cómo obliga TypeScript a manejar nuevos errores?
- ¿Dónde guardo los errores para que el agente los use?
- ¿Qué métricas debo instrumentar primero?
- ¿Cómo debo testear los manejadores de error?
- ¿Qué herramientas de validación recomiendan antes de llamar a una tool?
Porque ocultan la causa raíz y priven al agente de información accionable. Un mensaje opaco lleva a reintentos inútiles o a continuar el flujo como si la acción hubiera tenido éxito.
Es un patrón que devuelve un valor tipado indicando éxito o fallo ({ok: true; value} | {ok: false; error}). Obliga al consumidor a tratar explícitamente los errores y permite que orquestadores y LLMs actúen sobre causas concretas.
Mediante comprobaciones exhaustivas usando never en un switch sobre una unión discriminada. Si se añade un nuevo caso y no se maneja, el compilador falla.
Persiste fallos en la memoria episódica (ej. Redis) y en la memoria semántica si aplica (vector DB). El orquestador debe guardar tanto éxitos como fallos para proporcionar contexto al LLM.
Retries por tipo de error (P50/P95), frecuencia de errores no mapeados, tokens gastados en reintentos y casos escalados a humano.
Incluye tests que simulen fallos de DB, timeouts y errores de validación; el pipeline debe fallar si un nuevo error llega a producción sin handler.
Valida entradas con Zod o JSON Schema para reducir la superficie de errores y asegurar que los fallos restantes sean reales y manejables.
