Los errores en código async no funcionan como los síncronos
En JavaScript síncrono, un error lanzado sube por la pila de llamadas hasta que algún try/catch lo atrapa — o el programa revienta. El código asíncrono rompe ese modelo. Cuando una petición de red falla, la función que la inició ya retornó hace rato. Ya no hay pila por la que subir.
Las promesas resuelven esto dándole a los errores su propio canal. Una promesa puede cumplirse con un valor o rechazarse con un motivo. El rechazo es el equivalente asíncrono de lanzar un error. Todo lo que verás en esta página busca que esos rechazos aterricen en algún lugar que tú controles, en vez de desvanecerse sin dejar rastro.
El bloque try/catch se ejecuta y termina sin problema. El rechazo ocurre 50ms después, mucho tiempo después de que el try ya finalizó. Nada lo captura. Esa es la trampa.
try/catch vuelve a funcionar con await
En cuanto haces await sobre una promesa, un rechazo se convierte en un error lanzado dentro de la función async. Un try/catch que la envuelva lo atrapa igual que cualquier throw síncrono:
Este es el patrón al que deberías recurrir primero. await conecta el mundo asíncrono con la forma familiar de try/catch de toda la vida. Mete dentro del try las llamadas con await que pueden fallar, y gestiónalas en el catch.
Un detalle que conviene tener presente: solo queda cubierta la llamada que realmente se espera con await. Si disparas una promesa sin esperarla, los errores se escapan igual.
El bug más típico: olvidarse del await
Si invocas una función async sin await (o sin devolver su promesa), los rechazos se cuelan sin que el try/catch los atrape:
El bloque try termina sin problemas. El rechazo ocurre en el siguiente tick, sin nadie que lo capture. Verás el típico aviso de "unhandled promise rejection" en la consola.
La solución siempre es la misma: hacer await a la llamada, o return de la promesa para que quien la invoque pueda esperarla.
.catch() es la otra cara de la misma moneda
Si no quieres usar async/await, puedes manejar los rechazos encadenando .catch():
.catch(fn) es una forma abreviada de .then(undefined, fn). Atrapa cualquier rechazo que se haya producido antes en la cadena. Un .catch() al final de la cadena es el equivalente asíncrono de un try/catch de alto nivel: la última línea de defensa antes de que el rechazo pase a considerarse "no manejado".
Combinar ambos estilos no supone ningún problema. Un patrón habitual consiste en usar async/await dentro de una función y dejar que quien la llame le enganche un .catch():
fetch no rechaza cuando hay errores HTTP
Este detalle nos ha mordido a todos al menos una vez. fetch solo rechaza ante fallos de red: que falle la resolución DNS, que rechacen la conexión, que la petición se aborte. Una respuesta 404 o 500 cuenta como un fetch exitoso. La promesa se resuelve sin problema; lo que pasa es que se resuelve con una respuesta cuyo ok vale false.
Si quieres que los errores HTTP caigan en tu bloque catch, comprueba res.ok y lanza el error tú mismo:
Es código repetitivo que conviene extraer a un helper en cuanto te pilles escribiéndolo dos veces.
Promise.all falla rápido; Promise.allSettled no
Promise.all recibe un array de promesas y resuelve con un array de resultados — salvo que alguna rechace, en cuyo caso rechaza al instante con ese error. El resto de promesas siguen ejecutándose, pero sus resultados se descartan.
Fallar rápido es el comportamiento correcto cuando necesitas todos los resultados y un solo fallo deja la operación sin sentido. Cuando lo que quieres es conocer cada resultado sin importar si falla alguno —del tipo "intenta estas cinco subidas y dime cuáles funcionaron y cuáles no"—, usa Promise.allSettled:
allSettled nunca falla. Cada entrada es {status: "fulfilled", value} o {status: "rejected", reason}.
Relanzar errores y capturas específicas
No todos los errores deben manejarse en el mismo bloque. Un patrón habitual es capturar, revisar y relanzar cualquier cosa que no esperábamos:
Tragarte todos los errores con un catch (err) {} vacío es la mejor forma de esconder bugs de verdad. Captura solo lo que puedas manejar con sentido; lo demás, relánzalo.
unhandledrejection en JavaScript: tu última red de seguridad
Por más cuidado que pongas, tarde o temprano se te va a escapar algo. Tanto Node.js como los navegadores ofrecen un hook global para las promesas rechazadas que nadie llegó a capturar:
// Browser
window.addEventListener("unhandledrejection", event => {
console.error("no manejado:", event.reason);
event.preventDefault(); // silencia el warning por defecto de la consola
});
// Node.js
process.on("unhandledRejection", reason => {
console.error("no manejado:", reason);
});
Esto no sustituye un manejo correcto: es un último recurso para loguear o mandar telemetría. En el Node.js moderno, una promesa rechazada sin manejar termina tumbando el proceso por defecto, que suele ser justo lo que quieres en producción. Registra el error, deja que el proceso muera y reinícialo limpio.
Checklist práctico
Cada vez que una función async toque algo que pueda fallar, pregúntate:
- ¿Cada
awaitarriesgado está dentro de untry/catch, o la promesa devuelta la maneja quien llama con.catch()? - ¿Estoy realmente haciendo
awaitde la llamada, o se me coló un fire-and-forget sin querer? - En el caso concreto de
fetch, ¿estoy comprobandores.okantes de fiarme de la respuesta? - Cuando lanzo cosas en paralelo, ¿me conviene
Promise.allo más bienPromise.allSettled? - ¿Hay un
.catch()de último nivel o un handler deunhandledrejectionpara que nada se pierda en silencio?
Acierta en esos cinco puntos y tu código asíncrono dejará de sorprenderte con errores que se esfuman dentro del event loop.
Siguiente: módulos ES
Con esto cerramos el capítulo de asincronía y su manejo de errores. Ahora toca ver cómo se organiza el código JavaScript entre archivos: import, export y el sistema de módulos que sostiene cualquier proyecto moderno.
Preguntas frecuentes
¿Cómo se manejan los errores dentro de una función async?
Envuelve las llamadas con await en un bloque try/catch. Cualquier rechazo de una promesa esperada se convierte en un error lanzado que el catch recibe sin problema. También puedes dejar que el error se propague y manejarlo desde quien llama a la función, encadenando un .catch() a la promesa que devuelve.
¿Por qué mi try/catch no atrapa el error?
Casi siempre es porque el error ocurre en código que no estás esperando con await. Si llamas a una función async sin await (ni devuelves su promesa), cualquier rechazo se escapa del try/catch que la rodea. Regla simple: haz await o return de la promesa de la que quieras capturar errores.
¿Qué pasa si una promesa se rechaza y nadie la captura?
Tienes un unhandled rejection. Node.js dispara el evento unhandledRejection y, en versiones recientes, termina el proceso por defecto. En el navegador se dispara window.onunhandledrejection y verás un warning en la consola. En cualquier caso, la solución es añadir un .catch() o manejarlo con try/catch alrededor de un await.
¿Cómo gestiona los errores Promise.all?
Promise.all se rechaza en cuanto una de las promesas falla: las demás siguen ejecutándose, pero sus resultados se descartan. Si necesitas el resultado de todas, fallen o no, usa Promise.allSettled, que resuelve con un array de objetos {status, value} o {status, reason}.