Dominando Async/Await: Manejo Avanzado de Errores y Patrones de Reintento
Async/await transformó JavaScript asíncrono, pero el manejo adecuado de errores sigue siendo un desafío. Este artículo analiza una implementación robusta con lógica de reintento, manejo de timeouts, estrategias de fallback y patrones de recuperación de errores esenciales para aplicaciones en producción.

Introducción: Más allá del Async/Await básico
Aunque la sintaxis async/await hace que el código asíncrono sea legible, las aplicaciones en producción requieren manejo de errores sofisticado, mecanismos de reintento, timeouts y estrategias de fallback. Fallos de red, límites de tasa de API y errores transitorios demandan un código resiliente que maneje las fallas de manera elegante. Analicemos una implementación completa de async/await que aborda desafíos del mundo real.
El código original: Errores comunes
La implementación inicial tenía varios problemas críticos que podían causar fallos en producción. Veamos la versión corregida y entendamos qué estaba mal:
Código corregido: Un cliente API resiliente
class APIClient { ... } // Código completo omitido por brevedadErrores críticos corregidos
El código original tenía varios errores graves. Primero, el AbortController se creó dentro del bloque try pero necesitaba estar accesible en catch para una limpieza adecuada. Segundo, el método isRetryableError podía recibir parámetros null/undefined, causando errores en tiempo de ejecución — esto se corrigió agregando verificaciones de null. Tercero, AbortError no se distinguía de otros errores, lo que podía causar reintentos infinitos en cancelaciones manuales. Cuarto, el objeto error carecía del operador de encadenamiento opcional al acceder a error.message en handleFailure, lo que podía provocar fallos si error estaba indefinido.
Promise.race para implementación de timeout
El código utiliza Promise.race para implementar la funcionalidad de timeout de manera elegante. Esto enfrenta la petición fetch real contra un promise de timeout. El primero que se resuelva o rechace, gana. Cuando se activa el timeout, se llama a controller.abort() para cancelar la petición fetch, evitando consumo innecesario de ancho de banda y memoria. Este patrón es superior a simplemente envolver fetch en un timeout, ya que cancela activamente la petición subyacente en lugar de solo ignorarla.
Exponential Backoff con Jitter
El método calculateBackoff implementa un exponential backoff: cada reintento espera progresivamente más tiempo (1s, 2s, 4s, 8s). Esto evita sobrecargar un servidor débil. El jitter agregado (retraso aleatorio) previene el problema del thundering herd, donde múltiples clientes reintentan simultáneamente y podrían causar fallos en cascada. Esta combinación es el estándar de la industria para la lógica de reintento en sistemas distribuidos y es utilizada por AWS, Google Cloud y grandes APIs.
Lógica de reintento inteligente con comprobaciones de seguridad
El método mejorado isRetryableError ahora incluye comprobaciones de seguridad esenciales. Primero valida que al menos se proporcione un parámetro, evitando errores de referencia nula. Comprueba explícitamente AbortError y retorna false — las cancelaciones manuales nunca deben reintentarse. El método distingue entre errores transitorios que valen la pena reintentar (fallos de red, timeouts, errores de servidor 5xx, límites 429) y fallos permanentes (400 Bad Request, 404 Not Found, 401 Unauthorized). Esta toma de decisiones inteligente es crucial en sistemas de producción.
Contexto de error mejorado
El código corregido intenta analizar los cuerpos de respuesta de error, proporcionando más contexto cuando ocurren fallos. Adjunta el objeto response y el body parseado a los errores lanzados, permitiendo que el código que llama acceda a información detallada del error. El uso de optional chaining (?.) previene fallos al acceder a propiedades potencialmente undefined. Este enfoque de programación defensiva asegura que el cliente permanezca estable incluso frente a respuestas de API inesperadas.
Manejo de errores estructurado
En lugar de simplemente lanzar errores, esta implementación devuelve objetos de respuesta estructurados con flags de éxito, datos, mensajes de error y metadatos como conteo de intentos. Este enfoque proporciona al código llamante toda la información necesaria para tomar decisiones informadas: mostrar mensajes amigables, registrar errores correctamente o activar flujos alternativos. El formato de retorno consistente simplifica el manejo de errores en el resto de la aplicación.
Estrategias de fallback con seguridad
Cuando todos los reintentos fallan, handleFailure implementa una estrategia de degradación elegante con manejo adecuado de null. El operador de encadenamiento opcional asegura que el código no falle si error está undefined. Si existen datos de fallback (por ejemplo, de una solicitud previa exitosa o valores predeterminados), se devuelven en lugar de fallar por completo. Esto permite que las aplicaciones sigan funcionando incluso durante fallos completos de API. La bandera fromCache permite que la interfaz indique cuando los datos podrían estar desactualizados.
Patrón For Loop para reintentos
Usar un bucle for con instrucciones continue para la lógica de reintentos es más limpio que los enfoques recursivos o bucles while. Hace explícito el número máximo de reintentos, previene problemas de desbordamiento de pila y muestra claramente el flujo. El contador de intentos rastrea el progreso y ayuda con el logging. Salir del bucle ante errores no reintentables evita retrasos innecesarios. Este patrón es más legible y mantenible que las alternativas.
Manejo de alcance de AbortController
El código corregido declara AbortController a nivel de bucle, asegurando que sea accesible en cada iteración. Esto permite cancelaciones adecuadas en escenarios de timeout y error. Crear un nuevo AbortController para cada intento permite un control preciso sobre la cancelación de la solicitud. Fetch moderno con AbortController es el estándar para operaciones asíncronas cancelables en JavaScript, reemplazando patrones más antiguos como tokens de cancelación de promesas.
Cobertura completa de métodos HTTP
La implementación corregida agrega métodos PUT y DELETE además de GET y POST, proporcionando soporte completo de operaciones CRUD. Cada método configura correctamente el tipo de solicitud y, cuando corresponde, serializa el cuerpo a JSON. Esto hace que el cliente sea más versátil para interacciones reales con APIs donde se requieren todos los métodos HTTP.
Ejemplo de uso mejorado
La función de demostración ahora incluye una salida de consola más informativa con indicadores visuales (✓, ⚠, ℹ, ✗) que facilitan la lectura de los logs. Maneja correctamente la bandera fromCache para informar a los usuarios cuando los datos podrían estar desactualizados. El manejo de errores distingue entre respuestas degradadas y errores inesperados, proporcionando retroalimentación apropiada para cada escenario.
Consideraciones para producción
Las implementaciones en producción deberían agregar patrones de circuit breaker para evitar fallos en cascada, limitación de tasa para respetar cuotas de API, deduplicación de solicitudes para evitar llamadas redundantes y tipado de errores completo para diferentes modos de fallo. La integración con herramientas de monitoreo ayuda a rastrear tasas de reintento, patrones de fallo y métricas de rendimiento. La lógica de renovación de tokens de autenticación suele pertenecer a esta capa. Considere usar bibliotecas establecidas como Axios con interceptores o Ky para implementaciones probadas.
Conclusiones clave
- Siempre valide los parámetros en funciones de verificación de errores para evitar errores de referencia nula.
- Use optional chaining (?.) al acceder a propiedades potencialmente undefined para evitar fallos.
- Declare AbortController en el nivel de alcance apropiado para garantizar acceso adecuado a la limpieza.
- Distinga AbortError de otros errores: las cancelaciones manuales nunca deben reintentarse.
- Adjunte contexto de error detallado (response, body) a los errores lanzados para facilitar la depuración.
- Promise.race implementa timeouts elegantemente al enfrentar fetch contra un promise de timeout.
- Exponential backoff con jitter previene sobrecarga de servidores y problemas de thundering herd.
- Objetos de respuesta estructurados con flags de éxito proporcionan resultados consistentes e informativos.
- Las estrategias de fallback permiten degradación elegante durante fallos completos de API.
- Los bucles for proporcionan lógica de reintentos clara y segura para la pila, superior a la recursión.
- Cobertura completa de métodos HTTP (GET, POST, PUT, DELETE) hace que los clientes sean más versátiles.
- La programación defensiva con verificaciones de null y optional chaining previene fallos inesperados.